提交 6daf4d36 编写于 作者: C Christof Marti

Merge branch 'better-merge-extension'

......@@ -34,7 +34,8 @@ const extensions = [
'git',
'gulp',
'grunt',
'jake'
'jake',
'merge-conflict'
];
extensions.forEach(extension => npmInstall(`extensions/${extension}`));
......
......@@ -13,7 +13,6 @@ import { CommandCenter } from './commands';
import { StatusBarCommands } from './statusbar';
import { GitContentProvider } from './contentProvider';
import { AutoFetcher } from './autofetch';
import { MergeDecorator } from './merge';
import { Askpass } from './askpass';
import { toDisposable } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
......@@ -58,14 +57,12 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi
const provider = new GitSCMProvider(model, commandCenter, statusBarCommands);
const contentProvider = new GitContentProvider(model);
const autoFetcher = new AutoFetcher(model);
const mergeDecorator = new MergeDecorator(model);
disposables.push(
commandCenter,
provider,
contentProvider,
autoFetcher,
mergeDecorator,
model
);
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { window, workspace, Disposable, TextEditor, TextDocument, Range } from 'vscode';
import { Model, Status } from './model';
import { filterEvent } from './util';
import { debounce } from './decorators';
import { iterate } from './iterators';
function* lines(document: TextDocument): IterableIterator<string> {
for (let i = 0; i < document.lineCount; i++) {
yield document.lineAt(i).text;
}
}
const pattern = /^<<<<<<<|^=======|^>>>>>>>/;
function decorate(document: TextDocument): Range[] {
return iterate(lines(document))
.map((line, i) => pattern.test(line) ? i : null)
.filter(i => i !== null)
.map((i: number) => new Range(i, 1, i, 1))
.toArray();
}
class TextEditorMergeDecorator {
private static DecorationType = window.createTextEditorDecorationType({
backgroundColor: 'rgba(255, 139, 0, 0.3)',
isWholeLine: true,
dark: {
backgroundColor: 'rgba(235, 59, 0, 0.3)'
}
});
private uri: string;
private disposables: Disposable[] = [];
constructor(
private model: Model,
private editor: TextEditor
) {
this.uri = this.editor.document.uri.toString();
const onDidChange = filterEvent(workspace.onDidChangeTextDocument, e => e.document && e.document.uri.toString() === this.uri);
onDidChange(this.redecorate, this, this.disposables);
model.onDidChange(this.redecorate, this, this.disposables);
this.redecorate();
}
@debounce(300)
private redecorate(): void {
let decorations: Range[] = [];
if (window.visibleTextEditors.every(e => e !== this.editor)) {
this.dispose();
return;
}
if (this.model.mergeGroup.resources.some(r => r.type === Status.BOTH_MODIFIED && r.resourceUri.toString() === this.uri)) {
decorations = decorate(this.editor.document);
}
this.editor.setDecorations(TextEditorMergeDecorator.DecorationType, decorations);
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
export class MergeDecorator {
private textEditorDecorators: TextEditorMergeDecorator[] = [];
private disposables: Disposable[] = [];
constructor(private model: Model) {
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
this.onDidChangeVisibleTextEditors(window.visibleTextEditors);
}
private onDidChangeVisibleTextEditors(editors: TextEditor[]): void {
this.textEditorDecorators.forEach(d => d.dispose());
this.textEditorDecorators = editors.map(e => new TextEditorMergeDecorator(this.model, e));
}
dispose(): void {
this.textEditorDecorators.forEach(d => d.dispose());
this.disposables.forEach(d => d.dispose());
}
}
{
"name": "merge-conflict",
"publisher": "vscode",
"displayName": "merge-conflict",
"description": "Merge Conflict",
"version": "0.7.0",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"engines": {
"vscode": "^1.5.0"
},
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension",
"scripts": {
"compile": "gulp compile-extension:merge-conflict",
"watch": "gulp watch-extension:merge-conflict"
},
"contributes": {
"commands": [
{
"category": "%command.category%",
"title": "%command.accept.all-incoming%",
"command": "merge-conflict.accept.all-incoming"
},
{
"category": "%command.category%",
"title": "%command.accept.all-both%",
"command": "merge-conflict.accept.all-both"
},
{
"category": "%command.category%",
"title": "%command.accept.current%",
"command": "merge-conflict.accept.current"
},
{
"category": "%command.category%",
"title": "%command.accept.incoming%",
"command": "merge-conflict.accept.incoming"
},
{
"category": "%command.category%",
"title": "Accept selection",
"command": "merge-conflict.accept.selection"
},
{
"category": "%command.category%",
"title": "%command.accept.both%",
"command": "merge-conflict.accept.both"
},
{
"category": "%command.category%",
"title": "%command.next%",
"command": "merge-conflict.next"
},
{
"category": "%command.category%",
"title": "%command.previous%",
"command": "merge-conflict.previous"
},
{
"category": "%command.category%",
"title": "%command.compare%",
"command": "merge-conflict.compare"
}
],
"keybindings": [
{
"command": "merge-conflict.next",
"when": "editorTextFocus",
"key": "alt+m down"
},
{
"command": "merge-conflict.previous",
"when": "editorTextFocus",
"key": "alt+m up"
},
{
"command": "merge-conflict.accept.selection",
"when": "editorTextFocus",
"key": "alt+m enter"
},
{
"command": "merge-conflict.accept.current",
"when": "editorTextFocus",
"key": "alt+m 1"
},
{
"command": "merge-conflict.accept.incoming",
"when": "editorTextFocus",
"key": "alt+m 2"
},
{
"command": "merge-conflict.accept.both",
"when": "editorTextFocus",
"key": "alt+m 3"
}
],
"configuration": {
"title": "%config.title%",
"properties": {
"merge-conflict.codeLens.enabled": {
"type": "boolean",
"description": "%config.codeLensEnabled%",
"default": true
},
"merge-conflict.decorators.enabled": {
"type": "boolean",
"description": "%config.decoratorsEnabled%",
"default": true
}
}
}
},
"dependencies": {
"vscode-extension-telemetry": "^0.0.7",
"vscode-nls": "^2.0.2"
},
"devDependencies": {
"@types/mocha": "^2.2.41",
"@types/node": "^7.0.4",
"mocha": "^3.2.0"
}
}
\ No newline at end of file
{
"command.category": "Merge Conflict",
"command.accept.all-incoming": "Accept all incoming",
"command.accept.all-both": "Accept all both",
"command.accept.current": "Accept current",
"command.accept.incoming": "Accept incoming",
"command.accept.selection": "Accept selection",
"command.accept.both": "Accept Both",
"command.next": "Next conflict",
"command.previous": "Previous conflict",
"command.compare": "Compare current conflict",
"config.title": "Merge Conflict",
"config.codeLensEnabled": "Enable/disable merge conflict block CodeLens within editor",
"config.decoratorsEnabled": "Enable/disable merge conflict decorators within editor"
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as interfaces from './interfaces';
import { loadMessageBundle } from 'vscode-nls';
const localize = loadMessageBundle();
export default class MergeConflictCodeLensProvider implements vscode.CodeLensProvider, vscode.Disposable {
private codeLensRegistrationHandle: vscode.Disposable | null;
private config: interfaces.IExtensionConfiguration;
private tracker: interfaces.IDocumentMergeConflictTracker;
constructor(private context: vscode.ExtensionContext, trackerService: interfaces.IDocumentMergeConflictTrackerService) {
this.tracker = trackerService.createTracker('codelens');
}
begin(config: interfaces.IExtensionConfiguration) {
this.config = config;
if (this.config.enableCodeLens) {
this.registerCodeLensProvider();
}
}
configurationUpdated(updatedConfig: interfaces.IExtensionConfiguration) {
if (updatedConfig.enableCodeLens === false && this.codeLensRegistrationHandle) {
this.codeLensRegistrationHandle.dispose();
this.codeLensRegistrationHandle = null;
}
else if (updatedConfig.enableCodeLens === true && !this.codeLensRegistrationHandle) {
this.registerCodeLensProvider();
}
this.config = updatedConfig;
}
dispose() {
if (this.codeLensRegistrationHandle) {
this.codeLensRegistrationHandle.dispose();
this.codeLensRegistrationHandle = null;
}
}
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.CodeLens[] | null> {
if (!this.config || !this.config.enableCodeLens) {
return null;
}
let conflicts = await this.tracker.getConflicts(document);
if (!conflicts || conflicts.length === 0) {
return null;
}
let items: vscode.CodeLens[] = [];
conflicts.forEach(conflict => {
let acceptCurrentCommand: vscode.Command = {
command: 'merge-conflict.accept.current',
title: localize('acceptCurrentChange', 'Accept current change'),
arguments: ['known-conflict', conflict]
};
let acceptIncomingCommand: vscode.Command = {
command: 'merge-conflict.accept.incoming',
title: localize('acceptIncomingChange', 'Accept incoming change'),
arguments: ['known-conflict', conflict]
};
let acceptBothCommand: vscode.Command = {
command: 'merge-conflict.accept.both',
title: localize('acceptBothChanges', 'Accept both changes'),
arguments: ['known-conflict', conflict]
};
let diffCommand: vscode.Command = {
command: 'merge-conflict.compare',
title: localize('compareChanges', 'Compare changes'),
arguments: [conflict]
};
items.push(
new vscode.CodeLens(conflict.range, acceptCurrentCommand),
new vscode.CodeLens(conflict.range.with(conflict.range.start.with({ character: conflict.range.start.character + 1 })), acceptIncomingCommand),
new vscode.CodeLens(conflict.range.with(conflict.range.start.with({ character: conflict.range.start.character + 2 })), acceptBothCommand),
new vscode.CodeLens(conflict.range.with(conflict.range.start.with({ character: conflict.range.start.character + 3 })), diffCommand)
);
});
return items;
}
private registerCodeLensProvider() {
this.codeLensRegistrationHandle = vscode.languages.registerCodeLensProvider({ pattern: '**/*' }, this);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as interfaces from './interfaces';
import ContentProvider from './contentProvider';
import * as path from 'path';
import { loadMessageBundle } from 'vscode-nls';
const localize = loadMessageBundle();
const messages = {
cursorNotInConflict: 'Editor cursor is not within a merge conflict',
cursorOnSplitterRange: 'Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block',
noConflicts: 'No merge conflicts found in this file',
noOtherConflictsInThisFile: 'No other merge conflicts within this file'
};
interface IDocumentMergeConflictNavigationResults {
canNavigate: boolean;
conflict?: interfaces.IDocumentMergeConflict;
}
enum NavigationDirection {
Forwards,
Backwards
}
export default class CommandHandler implements vscode.Disposable {
private disposables: vscode.Disposable[] = [];
private tracker: interfaces.IDocumentMergeConflictTracker;
constructor(private context: vscode.ExtensionContext, trackerService: interfaces.IDocumentMergeConflictTrackerService) {
this.tracker = trackerService.createTracker('commands');
}
begin() {
this.disposables.push(
vscode.commands.registerTextEditorCommand('merge-conflict.accept.current', this.acceptCurrent, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.incoming', this.acceptIncoming, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.selection', this.acceptSelection, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.both', this.acceptBoth, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.all-current', this.acceptAllCurrent, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.all-incoming', this.acceptAllIncoming, this),
vscode.commands.registerTextEditorCommand('merge-conflict.accept.all-both', this.acceptAllBoth, this),
vscode.commands.registerTextEditorCommand('merge-conflict.next', this.navigateNext, this),
vscode.commands.registerTextEditorCommand('merge-conflict.previous', this.navigatePrevious, this),
vscode.commands.registerTextEditorCommand('merge-conflict.compare', this.compare, this)
);
}
acceptCurrent(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.accept(interfaces.CommitType.Current, editor, ...args);
}
acceptIncoming(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.accept(interfaces.CommitType.Incoming, editor, ...args);
}
acceptBoth(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.accept(interfaces.CommitType.Both, editor, ...args);
}
acceptAllCurrent(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.acceptAll(interfaces.CommitType.Current, editor);
}
acceptAllIncoming(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.acceptAll(interfaces.CommitType.Incoming, editor);
}
acceptAllBoth(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.acceptAll(interfaces.CommitType.Both, editor);
}
async compare(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, conflict: interfaces.IDocumentMergeConflict | null, ...args) {
const fileName = path.basename(editor.document.uri.fsPath);
// No conflict, command executed from command palette
if (!conflict) {
conflict = await this.findConflictContainingSelection(editor);
// Still failed to find conflict, warn the user and exit
if (!conflict) {
vscode.window.showWarningMessage(localize('cursorNotInConflict', messages.cursorNotInConflict));
return;
}
}
let range = conflict.current.content;
const leftUri = editor.document.uri.with({
scheme: ContentProvider.scheme,
query: JSON.stringify(range)
});
range = conflict.incoming.content;
const rightUri = leftUri.with({ query: JSON.stringify(range) });
const title = localize('compareChangesTitle', '{0}: Current changes \u2194 Incoming changes', fileName);
vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, title);
}
navigateNext(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.navigate(editor, NavigationDirection.Forwards);
}
navigatePrevious(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
return this.navigate(editor, NavigationDirection.Backwards);
}
async acceptSelection(editor: vscode.TextEditor, edit: vscode.TextEditorEdit, ...args): Promise<void> {
let conflict = await this.findConflictContainingSelection(editor);
if (!conflict) {
vscode.window.showWarningMessage(localize('cursorNotInConflict', messages.cursorNotInConflict));
return;
}
let typeToAccept: interfaces.CommitType;
// Figure out if the cursor is in current or incoming, we do this by seeing if
// the active position is before or after the range of the splitter. We can
// use this trick as the previous check in findConflictByActiveSelection will
// ensure it's within the conflict range, so we don't falsely identify "current"
// or "incoming" if outside of a conflict range.
if (editor.selection.active.isBefore(conflict.splitter.start)) {
typeToAccept = interfaces.CommitType.Current;
}
else if (editor.selection.active.isAfter(conflict.splitter.end)) {
typeToAccept = interfaces.CommitType.Incoming;
}
else {
vscode.window.showWarningMessage(localize('cursorOnSplitterRange', messages.cursorOnSplitterRange));
return;
}
this.tracker.forget(editor.document);
conflict.commitEdit(typeToAccept, editor);
}
dispose() {
this.disposables.forEach(disposable => disposable.dispose());
this.disposables = [];
}
private async navigate(editor: vscode.TextEditor, direction: NavigationDirection): Promise<void> {
let navigationResult = await this.findConflictForNavigation(editor, direction);
if (!navigationResult) {
vscode.window.showWarningMessage(localize('noConflicts', messages.noConflicts));
return;
}
else if (!navigationResult.canNavigate) {
vscode.window.showWarningMessage(localize('noOtherConflictsInThisFile', messages.noOtherConflictsInThisFile));
return;
}
else if (!navigationResult.conflict) {
// TODO: Show error message?
return;
}
// Move the selection to the first line of the conflict
editor.selection = new vscode.Selection(navigationResult.conflict.range.start, navigationResult.conflict.range.start);
editor.revealRange(navigationResult.conflict.range, vscode.TextEditorRevealType.Default);
}
private async accept(type: interfaces.CommitType, editor: vscode.TextEditor, ...args): Promise<void> {
let conflict: interfaces.IDocumentMergeConflict | null;
// If launched with known context, take the conflict from that
if (args[0] === 'known-conflict') {
conflict = args[1];
}
else {
// Attempt to find a conflict that matches the current curosr position
conflict = await this.findConflictContainingSelection(editor);
}
if (!conflict) {
vscode.window.showWarningMessage(localize('cursorNotInConflict', messages.cursorNotInConflict));
return;
}
// Tracker can forget as we know we are going to do an edit
this.tracker.forget(editor.document);
conflict.commitEdit(type, editor);
}
private async acceptAll(type: interfaces.CommitType, editor: vscode.TextEditor): Promise<void> {
let conflicts = await this.tracker.getConflicts(editor.document);
if (!conflicts || conflicts.length === 0) {
vscode.window.showWarningMessage(localize('noConflicts', messages.noConflicts));
return;
}
// For get the current state of the document, as we know we are doing to do a large edit
this.tracker.forget(editor.document);
// Apply all changes as one edit
await editor.edit((edit) => conflicts.forEach(conflict => {
conflict.applyEdit(type, editor, edit);
}));
}
private async findConflictContainingSelection(editor: vscode.TextEditor, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<interfaces.IDocumentMergeConflict | null> {
if (!conflicts) {
conflicts = await this.tracker.getConflicts(editor.document);
}
if (!conflicts || conflicts.length === 0) {
return null;
}
for (let i = 0; i < conflicts.length; i++) {
if (conflicts[i].range.contains(editor.selection.active)) {
return conflicts[i];
}
}
return null;
}
private async findConflictForNavigation(editor: vscode.TextEditor, direction: NavigationDirection, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<IDocumentMergeConflictNavigationResults | null> {
if (!conflicts) {
conflicts = await this.tracker.getConflicts(editor.document);
}
if (!conflicts || conflicts.length === 0) {
return null;
}
let selection = editor.selection.active;
if (conflicts.length === 1) {
if (conflicts[0].range.contains(selection)) {
return {
canNavigate: false
};
}
return {
canNavigate: true,
conflict: conflicts[0]
};
}
let predicate: (conflict) => boolean;
let fallback: () => interfaces.IDocumentMergeConflict;
if (direction === NavigationDirection.Forwards) {
predicate = (conflict) => selection.isBefore(conflict.range.start);
fallback = () => conflicts![0];
} else if (direction === NavigationDirection.Backwards) {
predicate = (conflict) => selection.isAfter(conflict.range.start);
fallback = () => conflicts![conflicts!.length - 1];
} else {
throw new Error(`Unsupported direction ${direction}`);
}
for (let i = 0; i < conflicts.length; i++) {
if (predicate(conflicts[i]) && !conflicts[i].range.contains(selection)) {
return {
canNavigate: true,
conflict: conflicts[i]
};
}
}
// Went all the way to the end, return the head
return {
canNavigate: true,
conflict: fallback()
};
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as interfaces from './interfaces';
export default class MergeConflictContentProvider implements vscode.TextDocumentContentProvider, vscode.Disposable {
static scheme = 'merge-conflict.conflict-diff';
constructor(private context: vscode.ExtensionContext) {
}
begin(config: interfaces.IExtensionConfiguration) {
this.context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(MergeConflictContentProvider.scheme, this)
);
}
dispose() {
}
async provideTextDocumentContent(uri: vscode.Uri): Promise<string | null> {
try {
const [start, end] = JSON.parse(uri.query) as { line: number, character: number }[];
const document = await vscode.workspace.openTextDocument(uri.with({ scheme: 'file', query: '' }));
const text = document.getText(new vscode.Range(start.line, start.character, end.line, end.character));
return text;
}
catch (ex) {
await vscode.window.showErrorMessage('Unable to show comparison');
return null;
}
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface ITask<T> {
(): T;
}
export class Delayer<T> {
public defaultDelay: number;
private timeout: any; // Timer
private completionPromise: Promise<T> | null;
private onSuccess: ((value?: T | Thenable<T> | null) => void) | null;
private task: ITask<T> | null;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onSuccess = null;
this.task = null;
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T> {
this.task = task;
if (delay >= 0) {
this.cancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T>((resolve) => {
this.onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
this.onSuccess = null;
var result = this.task!();
this.task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.onSuccess!(null);
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
}
public forceDelivery(): Promise<T> | null {
if (!this.completionPromise) {
return null;
}
this.cancelTimeout();
let result = this.completionPromise;
this.onSuccess!(null);
return result;
}
public isTriggered(): boolean {
return this.timeout !== null;
}
public cancel(): void {
this.cancelTimeout();
this.completionPromise = null;
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as interfaces from './interfaces';
import * as vscode from 'vscode';
export class DocumentMergeConflict implements interfaces.IDocumentMergeConflict {
public range: vscode.Range;
public current: interfaces.IMergeRegion;
public incoming: interfaces.IMergeRegion;
public splitter: vscode.Range;
constructor(document: vscode.TextDocument, descriptor: interfaces.IDocumentMergeConflictDescriptor) {
this.range = descriptor.range;
this.current = descriptor.current;
this.incoming = descriptor.incoming;
this.splitter = descriptor.splitter;
}
public commitEdit(type: interfaces.CommitType, editor: vscode.TextEditor, edit?: vscode.TextEditorEdit): Thenable<boolean> {
if (edit) {
this.applyEdit(type, editor, edit);
return Promise.resolve(true);
};
return editor.edit((edit) => this.applyEdit(type, editor, edit));
}
public applyEdit(type: interfaces.CommitType, editor: vscode.TextEditor, edit: vscode.TextEditorEdit): void {
// Each conflict is a set of ranges as follows, note placements or newlines
// which may not in in spans
// [ Conflict Range -- (Entire content below)
// [ Current Header ]\n -- >>>>> Header
// [ Current Content ] -- (content)
// [ Splitter ]\n -- =====
// [ Incoming Content ] -- (content)
// [ Incoming Header ]\n -- <<<<< Incoming
// ]
if (type === interfaces.CommitType.Current) {
// Replace [ Conflict Range ] with [ Current Content ]
let content = editor.document.getText(this.current.content);
this.replaceRangeWithContent(content, edit);
}
else if (type === interfaces.CommitType.Incoming) {
let content = editor.document.getText(this.incoming.content);
this.replaceRangeWithContent(content, edit);
}
else if (type === interfaces.CommitType.Both) {
// Replace [ Conflict Range ] with [ Current Content ] + \n + [ Incoming Content ]
const currentContent = editor.document.getText(this.current.content);
const incomingContent = editor.document.getText(this.incoming.content);
edit.replace(this.range, currentContent.concat(incomingContent));
}
}
private replaceRangeWithContent(content: string, edit: vscode.TextEditorEdit) {
if (this.isNewlineOnly(content)) {
edit.replace(this.range, '');
return;
}
// Replace [ Conflict Range ] with [ Current Content ]
edit.replace(this.range, content);
}
private isNewlineOnly(text: string) {
return text === '\n' || text === '\r\n';
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { MergeConflictParser } from './mergeConflictParser';
import * as interfaces from './interfaces';
import { Delayer } from './delayer';
class ScanTask {
public origins: Set<string> = new Set<string>();
public delayTask: Delayer<interfaces.IDocumentMergeConflict[]>;
constructor(delayTime: number, initialOrigin: string) {
this.origins.add(initialOrigin);
this.delayTask = new Delayer<interfaces.IDocumentMergeConflict[]>(delayTime);
}
public addOrigin(name: string): boolean {
if (this.origins.has(name)) {
return false;
}
return false;
}
public hasOrigin(name: string): boolean {
return this.origins.has(name);
}
}
class OriginDocumentMergeConflictTracker implements interfaces.IDocumentMergeConflictTracker {
constructor(private parent: DocumentMergeConflictTracker, private origin: string) {
}
getConflicts(document: vscode.TextDocument): PromiseLike<interfaces.IDocumentMergeConflict[]> {
return this.parent.getConflicts(document, this.origin);
}
isPending(document: vscode.TextDocument): boolean {
return this.parent.isPending(document, this.origin);
}
forget(document: vscode.TextDocument) {
this.parent.forget(document);
}
}
export default class DocumentMergeConflictTracker implements vscode.Disposable, interfaces.IDocumentMergeConflictTrackerService {
private cache: Map<string, ScanTask> = new Map();
private delayExpireTime: number = 250;
getConflicts(document: vscode.TextDocument, origin: string): PromiseLike<interfaces.IDocumentMergeConflict[]> {
// Attempt from cache
let key = this.getCacheKey(document);
if (!key) {
// Document doesnt have a uri, can't cache it, so return
return Promise.resolve(this.getConflictsOrEmpty(document, [origin]));
}
let cacheItem = this.cache.get(key);
if (!cacheItem) {
cacheItem = new ScanTask(this.delayExpireTime, origin);
this.cache.set(key, cacheItem);
}
else {
cacheItem.addOrigin(origin);
}
return cacheItem.delayTask.trigger(() => {
let conflicts = this.getConflictsOrEmpty(document, Array.from(cacheItem!.origins));
if (this.cache) {
this.cache.delete(key!);
}
return conflicts;
});
}
isPending(document: vscode.TextDocument, origin: string): boolean {
if (!document) {
return false;
}
let key = this.getCacheKey(document);
if (!key) {
return false;
}
var task = this.cache.get(key);
if (!task) {
return false;
}
return task.hasOrigin(origin);
}
createTracker(origin: string): interfaces.IDocumentMergeConflictTracker {
return new OriginDocumentMergeConflictTracker(this, origin);
}
forget(document: vscode.TextDocument) {
let key = this.getCacheKey(document);
if (key) {
this.cache.delete(key);
}
}
dispose() {
this.cache.clear();
}
private getConflictsOrEmpty(document: vscode.TextDocument, origins: string[]): interfaces.IDocumentMergeConflict[] {
const containsConflict = MergeConflictParser.containsConflict(document);
if (!containsConflict) {
return [];
}
const conflicts = MergeConflictParser.scanDocument(document);
return conflicts;
}
private getCacheKey(document: vscode.TextDocument): string | null {
if (document.uri && document.uri) {
return document.uri.toString();
}
return null;
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import MergeConflictServices from './services';
export function activate(context: vscode.ExtensionContext) {
// Register disposables
const services = new MergeConflictServices(context);
services.begin();
context.subscriptions.push(services);
}
export function deactivate() {
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface IMergeRegion {
name: string;
header: vscode.Range;
content: vscode.Range;
decoratorContent: vscode.Range;
}
export enum CommitType {
Current,
Incoming,
Both
}
export interface IExtensionConfiguration {
enableCodeLens: boolean;
enableDecorations: boolean;
enableEditorOverview: boolean;
}
export interface IDocumentMergeConflict extends IDocumentMergeConflictDescriptor {
commitEdit(type: CommitType, editor: vscode.TextEditor, edit?: vscode.TextEditorEdit);
applyEdit(type: CommitType, editor: vscode.TextEditor, edit: vscode.TextEditorEdit);
}
export interface IDocumentMergeConflictDescriptor {
range: vscode.Range;
current: IMergeRegion;
incoming: IMergeRegion;
splitter: vscode.Range;
}
export interface IDocumentMergeConflictTracker {
getConflicts(document: vscode.TextDocument): PromiseLike<IDocumentMergeConflict[]>;
isPending(document: vscode.TextDocument): boolean;
forget(document: vscode.TextDocument);
}
export interface IDocumentMergeConflictTrackerService {
createTracker(origin: string): IDocumentMergeConflictTracker;
forget(document: vscode.TextDocument);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as interfaces from './interfaces';
import { DocumentMergeConflict } from './documentMergeConflict';
const startHeaderMarker = '<<<<<<< ';
const splitterMarker = '=======';
const endFooterMarker = '>>>>>>> ';
interface IScanMergedConflict {
startHeader: vscode.TextLine;
splitter?: vscode.TextLine;
endFooter?: vscode.TextLine;
}
export class MergeConflictParser {
static scanDocument(document: vscode.TextDocument): interfaces.IDocumentMergeConflict[] {
// Scan each line in the document, we already know there is atleast a <<<<<<< and
// >>>>>> marker within the document, we need to group these into conflict ranges.
// We initially build a scan match, that references the lines of the header, splitter
// and footer. This is then converted into a full descriptor containing all required
// ranges.
let currentConflict: IScanMergedConflict | null = null;
const conflictDescriptors: interfaces.IDocumentMergeConflictDescriptor[] = [];
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
// Ignore empty lines
if (!line || line.isEmptyOrWhitespace) {
continue;
}
// Is this a start line? <<<<<<<
if (line.text.startsWith(startHeaderMarker)) {
if (currentConflict !== null) {
// Error, we should not see a startMarker before we've seen an endMarker
currentConflict = null;
// Give up parsing, anything matched up this to this point will be decorated
// anything after will not
break;
}
// Create a new conflict starting at this line
currentConflict = { startHeader: line };
}
// Are we within a conflict block and is this a splitter? =======
else if (currentConflict && line.text.startsWith(splitterMarker)) {
currentConflict.splitter = line;
}
// Are we withon a conflict block and is this a footer? >>>>>>>
else if (currentConflict && line.text.startsWith(endFooterMarker)) {
currentConflict.endFooter = line;
// Create a full descriptor from the lines that we matched. This can return
// null if the descriptor could not be completed.
let completeDescriptor = MergeConflictParser.scanItemTolMergeConflictDescriptor(document, currentConflict);
if (completeDescriptor !== null) {
conflictDescriptors.push(completeDescriptor);
}
// Reset the current conflict to be empty, so we can match the next
// starting header marker.
currentConflict = null;
}
}
return conflictDescriptors
.filter(Boolean)
.map(descriptor => new DocumentMergeConflict(document, descriptor));
}
private static scanItemTolMergeConflictDescriptor(document: vscode.TextDocument, scanned: IScanMergedConflict): interfaces.IDocumentMergeConflictDescriptor | null {
// Validate we have all the required lines within the scan item.
if (!scanned.startHeader || !scanned.splitter || !scanned.endFooter) {
return null;
}
// Assume that descriptor.current.header, descriptor.incoming.header and descriptor.spliiter
// have valid ranges, fill in content and total ranges from these parts.
// NOTE: We need to shift the decortator range back one character so the splitter does not end up with
// two decoration colors (current and splitter), if we take the new line from the content into account
// the decorator will wrap to the next line.
return {
current: {
header: scanned.startHeader.range,
decoratorContent: new vscode.Range(
scanned.startHeader.rangeIncludingLineBreak.end,
MergeConflictParser.shiftBackOneCharacter(document, scanned.splitter.range.start)),
// Current content is range between header (shifted for linebreak) and splitter start
content: new vscode.Range(
scanned.startHeader.rangeIncludingLineBreak.end,
scanned.splitter.range.start),
name: scanned.startHeader.text.substring(startHeaderMarker.length)
},
splitter: scanned.splitter.range,
incoming: {
header: scanned.endFooter.range,
decoratorContent: new vscode.Range(
scanned.splitter.rangeIncludingLineBreak.end,
MergeConflictParser.shiftBackOneCharacter(document, scanned.endFooter.range.start)),
// Incoming content is range between splitter (shifted for linebreak) and footer start
content: new vscode.Range(
scanned.splitter.rangeIncludingLineBreak.end,
scanned.endFooter.range.start),
name: scanned.endFooter.text.substring(endFooterMarker.length)
},
// Entire range is between current header start and incoming header end (including line break)
range: new vscode.Range(scanned.startHeader.range.start, scanned.endFooter.rangeIncludingLineBreak.end)
};
}
static containsConflict(document: vscode.TextDocument): boolean {
if (!document) {
return false;
}
let text = document.getText();
return text.includes(startHeaderMarker) && text.includes(endFooterMarker);
}
private static shiftBackOneCharacter(document: vscode.TextDocument, range: vscode.Position): vscode.Position {
let line = range.line;
let character = range.character - 1;
if (character < 0) {
line--;
character = document.lineAt(line).range.end.character;
}
return new vscode.Position(line, character);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as interfaces from './interfaces';
import { loadMessageBundle } from 'vscode-nls';
const localize = loadMessageBundle();
export default class MergeDectorator implements vscode.Disposable {
private decorations: { [key: string]: vscode.TextEditorDecorationType } = {};
private decorationUsesWholeLine: boolean = true; // Useful for debugging, set to false to see exact match ranges
// TODO: Move to config?
private currentColorRgb = `32,200,94`;
private incomingColorRgb = `24,134,255`;
private config: interfaces.IExtensionConfiguration;
private tracker: interfaces.IDocumentMergeConflictTracker;
constructor(private context: vscode.ExtensionContext, trackerService: interfaces.IDocumentMergeConflictTrackerService) {
this.tracker = trackerService.createTracker('decorator');
}
begin(config: interfaces.IExtensionConfiguration) {
this.config = config;
this.registerDecorationTypes(config);
// Check if we already have a set of active windows, attempt to track these.
vscode.window.visibleTextEditors.forEach(e => this.applyDecorations(e));
vscode.workspace.onDidOpenTextDocument(event => {
this.applyDecorationsFromEvent(event);
}, null, this.context.subscriptions);
vscode.workspace.onDidChangeTextDocument(event => {
this.applyDecorationsFromEvent(event.document);
}, null, this.context.subscriptions);
vscode.window.onDidChangeActiveTextEditor((e) => {
// New editor attempt to apply
this.applyDecorations(e);
}, null, this.context.subscriptions);
}
configurationUpdated(config: interfaces.IExtensionConfiguration) {
this.config = config;
this.registerDecorationTypes(config);
// Re-apply the decoration
vscode.window.visibleTextEditors.forEach(e => {
this.removeDecorations(e);
this.applyDecorations(e);
});
}
private registerDecorationTypes(config: interfaces.IExtensionConfiguration) {
// Dispose of existing decorations
Object.keys(this.decorations).forEach(k => this.decorations[k].dispose());
this.decorations = {};
// None of our features are enabled
if (!config.enableDecorations || !config.enableEditorOverview) {
return;
}
// Create decorators
if (config.enableDecorations || config.enableEditorOverview) {
this.decorations['current.content'] = vscode.window.createTextEditorDecorationType(
this.generateBlockRenderOptions(this.currentColorRgb, config)
);
this.decorations['incoming.content'] = vscode.window.createTextEditorDecorationType(
this.generateBlockRenderOptions(this.incomingColorRgb, config)
);
}
if (config.enableDecorations) {
this.decorations['current.header'] = vscode.window.createTextEditorDecorationType({
// backgroundColor: 'rgba(255, 0, 0, 0.01)',
// border: '2px solid red',
isWholeLine: this.decorationUsesWholeLine,
backgroundColor: `rgba(${this.currentColorRgb}, 1.0)`,
color: 'white',
after: {
contentText: ' ' + localize('currentChange', '(Current change)'),
color: 'rgba(0, 0, 0, 0.7)'
}
});
this.decorations['splitter'] = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0, 0, 0, 0.25)',
color: 'white',
isWholeLine: this.decorationUsesWholeLine,
});
this.decorations['incoming.header'] = vscode.window.createTextEditorDecorationType({
backgroundColor: `rgba(${this.incomingColorRgb}, 1.0)`,
color: 'white',
isWholeLine: this.decorationUsesWholeLine,
after: {
contentText: ' ' + localize('incomingChange', '(Incoming change)'),
color: 'rgba(0, 0, 0, 0.7)'
}
});
}
}
dispose() {
// TODO: Replace with Map<string, T>
Object.keys(this.decorations).forEach(name => {
this.decorations[name].dispose();
});
this.decorations = {};
}
private generateBlockRenderOptions(color: string, config: interfaces.IExtensionConfiguration): vscode.DecorationRenderOptions {
let renderOptions: any = {};
if (config.enableDecorations) {
renderOptions.backgroundColor = `rgba(${color}, 0.2)`;
renderOptions.isWholeLine = this.decorationUsesWholeLine;
}
if (config.enableEditorOverview) {
renderOptions.overviewRulerColor = `rgba(${color}, 0.5)`;
renderOptions.overviewRulerLane = vscode.OverviewRulerLane.Full;
}
return renderOptions;
}
private applyDecorationsFromEvent(eventDocument: vscode.TextDocument) {
for (var i = 0; i < vscode.window.visibleTextEditors.length; i++) {
if (vscode.window.visibleTextEditors[i].document === eventDocument) {
// Attempt to apply
this.applyDecorations(vscode.window.visibleTextEditors[i]);
}
}
}
private async applyDecorations(editor: vscode.TextEditor) {
if (!editor || !editor.document) { return; }
if (!this.config || (!this.config.enableDecorations && !this.config.enableEditorOverview)) {
return;
}
// If we have a pending scan from the same origin, exit early.
if (this.tracker.isPending(editor.document)) {
return;
}
let conflicts = await this.tracker.getConflicts(editor.document);
if (conflicts.length === 0) {
this.removeDecorations(editor);
return;
}
// Store decorations keyed by the type of decoration, set decoration wants a "style"
// to go with it, which will match this key (see constructor);
let matchDecorations: { [key: string]: vscode.DecorationOptions[] } = {};
let pushDecoration = (key: string, d: vscode.DecorationOptions) => {
matchDecorations[key] = matchDecorations[key] || [];
matchDecorations[key].push(d);
};
conflicts.forEach(conflict => {
// TODO, this could be more effective, just call getMatchPositions once with a map of decoration to position
pushDecoration('current.content', { range: conflict.current.decoratorContent });
pushDecoration('incoming.content', { range: conflict.incoming.decoratorContent });
if (this.config.enableDecorations) {
pushDecoration('current.header', { range: conflict.current.header });
pushDecoration('splitter', { range: conflict.splitter });
pushDecoration('incoming.header', { range: conflict.incoming.header });
}
});
// For each match we've generated, apply the generated decoration with the matching decoration type to the
// editor instance. Keys in both matches and decorations should match.
Object.keys(matchDecorations).forEach(decorationKey => {
let decorationType = this.decorations[decorationKey];
if (decorationType) {
editor.setDecorations(decorationType, matchDecorations[decorationKey]);
}
});
}
private removeDecorations(editor: vscode.TextEditor) {
// Remove all decorations, there might be none
Object.keys(this.decorations).forEach(decorationKey => {
// Race condition, while editing the settings, it's possible to
// generate regions before the configuration has been refreshed
let decorationType = this.decorations[decorationKey];
if (decorationType) {
editor.setDecorations(decorationType, []);
}
});
}
}
\ No newline at end of file
此差异已折叠。
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import DocumentTracker from './documentTracker';
import CodeLensProvider from './codelensProvider';
import CommandHandler from './commandHandler';
import ContentProvider from './contentProvider';
import Decorator from './mergeDecorator';
import * as interfaces from './interfaces';
const ConfigurationSectionName = 'merge-conflict';
export default class ServiceWrapper implements vscode.Disposable {
private services: vscode.Disposable[] = [];
constructor(private context: vscode.ExtensionContext) {
}
begin() {
let configuration = this.createExtensionConfiguration();
const documentTracker = new DocumentTracker();
this.services.push(
documentTracker,
new CommandHandler(this.context, documentTracker),
new CodeLensProvider(this.context, documentTracker),
new ContentProvider(this.context),
new Decorator(this.context, documentTracker),
);
this.services.forEach((service: any) => {
if (service.begin && service.begin instanceof Function) {
service.begin(configuration);
}
});
vscode.workspace.onDidChangeConfiguration(() => {
this.services.forEach((service: any) => {
if (service.configurationUpdated && service.configurationUpdated instanceof Function) {
service.configurationUpdated(this.createExtensionConfiguration());
}
});
});
}
createExtensionConfiguration(): interfaces.IExtensionConfiguration {
const workspaceConfiguration = vscode.workspace.getConfiguration(ConfigurationSectionName);
const codeLensEnabled: boolean = workspaceConfiguration.get('codeLens.enabled', true);
const decoratorsEnabled: boolean = workspaceConfiguration.get('decorators.enabled', true);
return {
enableCodeLens: codeLensEnabled,
enableDecorations: decoratorsEnabled,
enableEditorOverview: decoratorsEnabled
};
}
dispose() {
this.services.forEach(disposable => disposable.dispose());
this.services = [];
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference types='@types/node'/>
/// <reference types='@types/mocha'/>
{
"compilerOptions": {
"target": "es6",
"lib": [
"es2016"
],
"module": "commonjs",
"outDir": "./out",
"strictNullChecks": true,
"experimentalDecorators": true
},
"include": [
"src/**/*"
]
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册