Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
106
lib/vscode/extensions/merge-conflict/src/codelensProvider.ts
Normal file
106
lib/vscode/extensions/merge-conflict/src/codelensProvider.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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(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([
|
||||
{ scheme: 'file' },
|
||||
{ scheme: 'untitled' },
|
||||
{ scheme: 'vscode-userdata' },
|
||||
], this);
|
||||
}
|
||||
}
|
368
lib/vscode/extensions/merge-conflict/src/commandHandler.ts
Normal file
368
lib/vscode/extensions/merge-conflict/src/commandHandler.ts
Normal file
@ -0,0 +1,368 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { loadMessageBundle } from 'vscode-nls';
|
||||
const localize = loadMessageBundle();
|
||||
|
||||
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(trackerService: interfaces.IDocumentMergeConflictTrackerService) {
|
||||
this.tracker = trackerService.createTracker('commands');
|
||||
}
|
||||
|
||||
begin() {
|
||||
this.disposables.push(
|
||||
this.registerTextEditorCommand('merge-conflict.accept.current', this.acceptCurrent),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.incoming', this.acceptIncoming),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.selection', this.acceptSelection),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.both', this.acceptBoth),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.all-current', this.acceptAllCurrent, this.acceptAllCurrentResources),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.all-incoming', this.acceptAllIncoming, this.acceptAllIncomingResources),
|
||||
this.registerTextEditorCommand('merge-conflict.accept.all-both', this.acceptAllBoth),
|
||||
this.registerTextEditorCommand('merge-conflict.next', this.navigateNext),
|
||||
this.registerTextEditorCommand('merge-conflict.previous', this.navigatePrevious),
|
||||
this.registerTextEditorCommand('merge-conflict.compare', this.compare)
|
||||
);
|
||||
}
|
||||
|
||||
private registerTextEditorCommand(command: string, cb: (editor: vscode.TextEditor, ...args: any[]) => Promise<void>, resourceCB?: (uris: vscode.Uri[]) => Promise<void>) {
|
||||
return vscode.commands.registerCommand(command, (...args) => {
|
||||
if (resourceCB && args.length && args.every(arg => arg && arg.resourceUri)) {
|
||||
return resourceCB.call(this, args.map(arg => arg.resourceUri));
|
||||
}
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
return editor && cb.call(this, editor, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
acceptCurrent(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
|
||||
return this.accept(interfaces.CommitType.Current, editor, ...args);
|
||||
}
|
||||
|
||||
acceptIncoming(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
|
||||
return this.accept(interfaces.CommitType.Incoming, editor, ...args);
|
||||
}
|
||||
|
||||
acceptBoth(editor: vscode.TextEditor, ...args: any[]): Promise<void> {
|
||||
return this.accept(interfaces.CommitType.Both, editor, ...args);
|
||||
}
|
||||
|
||||
acceptAllCurrent(editor: vscode.TextEditor): Promise<void> {
|
||||
return this.acceptAll(interfaces.CommitType.Current, editor);
|
||||
}
|
||||
|
||||
acceptAllIncoming(editor: vscode.TextEditor): Promise<void> {
|
||||
return this.acceptAll(interfaces.CommitType.Incoming, editor);
|
||||
}
|
||||
|
||||
acceptAllCurrentResources(resources: vscode.Uri[]): Promise<void> {
|
||||
return this.acceptAllResources(interfaces.CommitType.Current, resources);
|
||||
}
|
||||
|
||||
acceptAllIncomingResources(resources: vscode.Uri[]): Promise<void> {
|
||||
return this.acceptAllResources(interfaces.CommitType.Incoming, resources);
|
||||
}
|
||||
|
||||
acceptAllBoth(editor: vscode.TextEditor): Promise<void> {
|
||||
return this.acceptAll(interfaces.CommitType.Both, editor);
|
||||
}
|
||||
|
||||
async compare(editor: vscode.TextEditor, conflict: interfaces.IDocumentMergeConflict | null) {
|
||||
|
||||
// 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', 'Editor cursor is not within a merge conflict'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const conflicts = await this.tracker.getConflicts(editor.document);
|
||||
|
||||
// Still failed to find conflict, warn the user and exit
|
||||
if (!conflicts) {
|
||||
vscode.window.showWarningMessage(localize('cursorNotInConflict', 'Editor cursor is not within a merge conflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
const scheme = editor.document.uri.scheme;
|
||||
let range = conflict.current.content;
|
||||
let leftRanges = conflicts.map(conflict => [conflict.current.content, conflict.range]);
|
||||
let rightRanges = conflicts.map(conflict => [conflict.incoming.content, conflict.range]);
|
||||
|
||||
const leftUri = editor.document.uri.with({
|
||||
scheme: ContentProvider.scheme,
|
||||
query: JSON.stringify({ scheme, range: range, ranges: leftRanges })
|
||||
});
|
||||
|
||||
|
||||
range = conflict.incoming.content;
|
||||
const rightUri = leftUri.with({ query: JSON.stringify({ scheme, ranges: rightRanges }) });
|
||||
|
||||
let mergeConflictLineOffsets = 0;
|
||||
for (let nextconflict of conflicts) {
|
||||
if (nextconflict.range.isEqual(conflict.range)) {
|
||||
break;
|
||||
} else {
|
||||
mergeConflictLineOffsets += (nextconflict.range.end.line - nextconflict.range.start.line) - (nextconflict.incoming.content.end.line - nextconflict.incoming.content.start.line);
|
||||
}
|
||||
}
|
||||
const selection = new vscode.Range(
|
||||
conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character,
|
||||
conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character
|
||||
);
|
||||
|
||||
const docPath = editor.document.uri.path;
|
||||
const fileName = docPath.substring(docPath.lastIndexOf('/') + 1); // avoid NodeJS path to keep browser webpack small
|
||||
const title = localize('compareChangesTitle', '{0}: Current Changes ⟷ Incoming Changes', fileName);
|
||||
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
|
||||
const openToTheSide = mergeConflictConfig.get<string>('diffViewPosition');
|
||||
const opts: vscode.TextDocumentShowOptions = {
|
||||
viewColumn: openToTheSide === 'Beside' ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
|
||||
selection
|
||||
};
|
||||
|
||||
if (openToTheSide === 'Below') {
|
||||
await vscode.commands.executeCommand('workbench.action.newGroupBelow');
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, title, opts);
|
||||
}
|
||||
|
||||
navigateNext(editor: vscode.TextEditor): Promise<void> {
|
||||
return this.navigate(editor, NavigationDirection.Forwards);
|
||||
}
|
||||
|
||||
navigatePrevious(editor: vscode.TextEditor): Promise<void> {
|
||||
return this.navigate(editor, NavigationDirection.Backwards);
|
||||
}
|
||||
|
||||
async acceptSelection(editor: vscode.TextEditor): Promise<void> {
|
||||
let conflict = await this.findConflictContainingSelection(editor);
|
||||
|
||||
if (!conflict) {
|
||||
vscode.window.showWarningMessage(localize('cursorNotInConflict', 'Editor cursor is not within a merge conflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
let typeToAccept: interfaces.CommitType;
|
||||
let tokenAfterCurrentBlock: vscode.Range = conflict.splitter;
|
||||
|
||||
if (conflict.commonAncestors.length > 0) {
|
||||
tokenAfterCurrentBlock = conflict.commonAncestors[0].header;
|
||||
}
|
||||
|
||||
// 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 or common
|
||||
// ancestors marker. 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(tokenAfterCurrentBlock.start)) {
|
||||
typeToAccept = interfaces.CommitType.Current;
|
||||
}
|
||||
else if (editor.selection.active.isAfter(conflict.splitter.end)) {
|
||||
typeToAccept = interfaces.CommitType.Incoming;
|
||||
}
|
||||
else if (editor.selection.active.isBefore(conflict.splitter.start)) {
|
||||
vscode.window.showWarningMessage(localize('cursorOnCommonAncestorsRange', 'Editor cursor is within the common ancestors block, please move it to either the "current" or "incoming" block'));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
vscode.window.showWarningMessage(localize('cursorOnSplitterRange', 'Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block'));
|
||||
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) {
|
||||
// Check for autoNavigateNextConflict, if it's enabled(which indicating no conflict remain), then do not show warning
|
||||
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
|
||||
if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {
|
||||
return;
|
||||
}
|
||||
vscode.window.showWarningMessage(localize('noConflicts', 'No merge conflicts found in this file'));
|
||||
return;
|
||||
}
|
||||
else if (!navigationResult.canNavigate) {
|
||||
vscode.window.showWarningMessage(localize('noOtherConflictsInThisFile', 'No other merge conflicts within this file'));
|
||||
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: any[]): 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 cursor position
|
||||
conflict = await this.findConflictContainingSelection(editor);
|
||||
}
|
||||
|
||||
if (!conflict) {
|
||||
vscode.window.showWarningMessage(localize('cursorNotInConflict', 'Editor cursor is not within a merge conflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Tracker can forget as we know we are going to do an edit
|
||||
this.tracker.forget(editor.document);
|
||||
conflict.commitEdit(type, editor);
|
||||
|
||||
// navigate to the next merge conflict
|
||||
const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');
|
||||
if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {
|
||||
this.navigateNext(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', 'No merge conflicts found in this file'));
|
||||
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.document, edit);
|
||||
}));
|
||||
}
|
||||
|
||||
private async acceptAllResources(type: interfaces.CommitType, resources: vscode.Uri[]): Promise<void> {
|
||||
const documents = await Promise.all(resources.map(resource => vscode.workspace.openTextDocument(resource)));
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
for (const document of documents) {
|
||||
const conflicts = await this.tracker.getConflicts(document);
|
||||
|
||||
if (!conflicts || conflicts.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For get the current state of the document, as we know we are doing to do a large edit
|
||||
this.tracker.forget(document);
|
||||
|
||||
// Apply all changes as one edit
|
||||
conflicts.forEach(conflict => {
|
||||
conflict.applyEdit(type, document, { replace: (range, newText) => edit.replace(document.uri, range, newText) });
|
||||
});
|
||||
}
|
||||
vscode.workspace.applyEdit(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 (const conflict of conflicts) {
|
||||
if (conflict.range.contains(editor.selection.active)) {
|
||||
return conflict;
|
||||
}
|
||||
}
|
||||
|
||||
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: any) => 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 (const conflict of conflicts) {
|
||||
if (predicate(conflict) && !conflict.range.contains(selection)) {
|
||||
return {
|
||||
canNavigate: true,
|
||||
conflict: conflict
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Went all the way to the end, return the head
|
||||
return {
|
||||
canNavigate: true,
|
||||
conflict: fallback()
|
||||
};
|
||||
}
|
||||
}
|
54
lib/vscode/extensions/merge-conflict/src/contentProvider.ts
Normal file
54
lib/vscode/extensions/merge-conflict/src/contentProvider.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 default class MergeConflictContentProvider implements vscode.TextDocumentContentProvider, vscode.Disposable {
|
||||
|
||||
static scheme = 'merge-conflict.conflict-diff';
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
begin() {
|
||||
this.context.subscriptions.push(
|
||||
vscode.workspace.registerTextDocumentContentProvider(MergeConflictContentProvider.scheme, this)
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
}
|
||||
|
||||
async provideTextDocumentContent(uri: vscode.Uri): Promise<string | null> {
|
||||
try {
|
||||
const { scheme, ranges } = JSON.parse(uri.query) as { scheme: string, ranges: [{ line: number, character: number }[], { line: number, character: number }[]][] };
|
||||
|
||||
// complete diff
|
||||
const document = await vscode.workspace.openTextDocument(uri.with({ scheme, query: '' }));
|
||||
|
||||
let text = '';
|
||||
let lastPosition = new vscode.Position(0, 0);
|
||||
|
||||
ranges.forEach(rangeObj => {
|
||||
let [conflictRange, fullRange] = rangeObj;
|
||||
const [start, end] = conflictRange;
|
||||
const [fullStart, fullEnd] = fullRange;
|
||||
|
||||
text += document.getText(new vscode.Range(lastPosition.line, lastPosition.character, fullStart.line, fullStart.character));
|
||||
text += document.getText(new vscode.Range(start.line, start.character, end.line, end.character));
|
||||
lastPosition = new vscode.Position(fullEnd.line, fullEnd.character);
|
||||
});
|
||||
|
||||
let documentEnd = document.lineAt(document.lineCount - 1).range.end;
|
||||
text += document.getText(new vscode.Range(lastPosition.line, lastPosition.character, documentEnd.line, documentEnd.character));
|
||||
|
||||
return text;
|
||||
}
|
||||
catch (ex) {
|
||||
await vscode.window.showErrorMessage('Unable to show comparison');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
79
lib/vscode/extensions/merge-conflict/src/delayer.ts
Normal file
79
lib/vscode/extensions/merge-conflict/src/delayer.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 | PromiseLike<T> | undefined) => 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 | undefined>((resolve) => {
|
||||
this.onSuccess = resolve;
|
||||
}).then(() => {
|
||||
this.completionPromise = null;
|
||||
this.onSuccess = null;
|
||||
let result = this.task!();
|
||||
this.task = null;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
if (delay >= 0 || this.timeout === null) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = null;
|
||||
this.onSuccess!(undefined);
|
||||
}, 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!(undefined);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 commonAncestors: interfaces.IMergeRegion[];
|
||||
public splitter: vscode.Range;
|
||||
|
||||
constructor(descriptor: interfaces.IDocumentMergeConflictDescriptor) {
|
||||
this.range = descriptor.range;
|
||||
this.current = descriptor.current;
|
||||
this.incoming = descriptor.incoming;
|
||||
this.commonAncestors = descriptor.commonAncestors;
|
||||
this.splitter = descriptor.splitter;
|
||||
}
|
||||
|
||||
public commitEdit(type: interfaces.CommitType, editor: vscode.TextEditor, edit?: vscode.TextEditorEdit): Thenable<boolean> {
|
||||
|
||||
if (edit) {
|
||||
|
||||
this.applyEdit(type, editor.document, edit);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return editor.edit((edit) => this.applyEdit(type, editor.document, edit));
|
||||
}
|
||||
|
||||
public applyEdit(type: interfaces.CommitType, document: vscode.TextDocument, edit: { replace(range: vscode.Range, newText: string): void; }): void {
|
||||
|
||||
// Each conflict is a set of ranges as follows, note placements or newlines
|
||||
// which may not 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 = document.getText(this.current.content);
|
||||
this.replaceRangeWithContent(content, edit);
|
||||
}
|
||||
else if (type === interfaces.CommitType.Incoming) {
|
||||
let content = 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 = document.getText(this.current.content);
|
||||
const incomingContent = document.getText(this.incoming.content);
|
||||
|
||||
edit.replace(this.range, currentContent.concat(incomingContent));
|
||||
}
|
||||
}
|
||||
|
||||
private replaceRangeWithContent(content: string, edit: { replace(range: vscode.Range, newText: string): void; }) {
|
||||
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';
|
||||
}
|
||||
}
|
137
lib/vscode/extensions/merge-conflict/src/documentTracker.ts
Normal file
137
lib/vscode/extensions/merge-conflict/src/documentTracker.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 = 0;
|
||||
|
||||
getConflicts(document: vscode.TextDocument, origin: string): PromiseLike<interfaces.IDocumentMergeConflict[]> {
|
||||
// Attempt from cache
|
||||
|
||||
let key = this.getCacheKey(document);
|
||||
|
||||
if (!key) {
|
||||
// Document doesn't 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;
|
||||
}
|
||||
|
||||
const 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) {
|
||||
return document.uri.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
48
lib/vscode/extensions/merge-conflict/src/interfaces.ts
Normal file
48
lib/vscode/extensions/merge-conflict/src/interfaces.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 const 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): Thenable<boolean>;
|
||||
applyEdit(type: CommitType, document: vscode.TextDocument, edit: { replace(range: vscode.Range, newText: string): void; }): void;
|
||||
}
|
||||
|
||||
export interface IDocumentMergeConflictDescriptor {
|
||||
range: vscode.Range;
|
||||
current: IMergeRegion;
|
||||
incoming: IMergeRegion;
|
||||
commonAncestors: IMergeRegion[];
|
||||
splitter: vscode.Range;
|
||||
}
|
||||
|
||||
export interface IDocumentMergeConflictTracker {
|
||||
getConflicts(document: vscode.TextDocument): PromiseLike<IDocumentMergeConflict[]>;
|
||||
isPending(document: vscode.TextDocument): boolean;
|
||||
forget(document: vscode.TextDocument): void;
|
||||
}
|
||||
|
||||
export interface IDocumentMergeConflictTrackerService {
|
||||
createTracker(origin: string): IDocumentMergeConflictTracker;
|
||||
forget(document: vscode.TextDocument): void;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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() {
|
||||
}
|
||||
|
168
lib/vscode/extensions/merge-conflict/src/mergeConflictParser.ts
Normal file
168
lib/vscode/extensions/merge-conflict/src/mergeConflictParser.ts
Normal file
@ -0,0 +1,168 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 commonAncestorsMarker = '|||||||';
|
||||
const splitterMarker = '=======';
|
||||
const endFooterMarker = '>>>>>>>';
|
||||
|
||||
interface IScanMergedConflict {
|
||||
startHeader: vscode.TextLine;
|
||||
commonAncestors: 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 at least 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, commonAncestors: [] };
|
||||
}
|
||||
// Are we within a conflict block and is this a common ancestors marker? |||||||
|
||||
else if (currentConflict && !currentConflict.splitter && line.text.startsWith(commonAncestorsMarker)) {
|
||||
currentConflict.commonAncestors.push(line);
|
||||
}
|
||||
// Are we within a conflict block and is this a splitter? =======
|
||||
else if (currentConflict && !currentConflict.splitter && line.text.startsWith(splitterMarker)) {
|
||||
currentConflict.splitter = line;
|
||||
}
|
||||
// Are we within 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(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;
|
||||
}
|
||||
|
||||
let tokenAfterCurrentBlock: vscode.TextLine = scanned.commonAncestors[0] || scanned.splitter;
|
||||
|
||||
// Assume that descriptor.current.header, descriptor.incoming.header and descriptor.splitter
|
||||
// have valid ranges, fill in content and total ranges from these parts.
|
||||
// NOTE: We need to shift the decorator 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, tokenAfterCurrentBlock.range.start, scanned.startHeader.rangeIncludingLineBreak.end)),
|
||||
// Current content is range between header (shifted for linebreak) and splitter or common ancestors mark start
|
||||
content: new vscode.Range(
|
||||
scanned.startHeader.rangeIncludingLineBreak.end,
|
||||
tokenAfterCurrentBlock.range.start),
|
||||
name: scanned.startHeader.text.substring(startHeaderMarker.length + 1)
|
||||
},
|
||||
commonAncestors: scanned.commonAncestors.map((currentTokenLine, index, commonAncestors) => {
|
||||
let nextTokenLine = commonAncestors[index + 1] || scanned.splitter;
|
||||
return {
|
||||
header: currentTokenLine.range,
|
||||
decoratorContent: new vscode.Range(
|
||||
currentTokenLine.rangeIncludingLineBreak.end,
|
||||
MergeConflictParser.shiftBackOneCharacter(document, nextTokenLine.range.start, currentTokenLine.rangeIncludingLineBreak.end)),
|
||||
// Each common ancestors block is range between one common ancestors token
|
||||
// (shifted for linebreak) and start of next common ancestors token or splitter
|
||||
content: new vscode.Range(
|
||||
currentTokenLine.rangeIncludingLineBreak.end,
|
||||
nextTokenLine.range.start),
|
||||
name: currentTokenLine.text.substring(commonAncestorsMarker.length + 1)
|
||||
};
|
||||
}),
|
||||
splitter: scanned.splitter.range,
|
||||
incoming: {
|
||||
header: scanned.endFooter.range,
|
||||
decoratorContent: new vscode.Range(
|
||||
scanned.splitter.rangeIncludingLineBreak.end,
|
||||
MergeConflictParser.shiftBackOneCharacter(document, scanned.endFooter.range.start, scanned.splitter.rangeIncludingLineBreak.end)),
|
||||
// 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 + 1)
|
||||
},
|
||||
// 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, unlessEqual: vscode.Position): vscode.Position {
|
||||
if (range.isEqual(unlessEqual)) {
|
||||
return range;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
252
lib/vscode/extensions/merge-conflict/src/mergeDecorator.ts
Normal file
252
lib/vscode/extensions/merge-conflict/src/mergeDecorator.ts
Normal file
@ -0,0 +1,252 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 MergeDecorator implements vscode.Disposable {
|
||||
|
||||
private decorations: { [key: string]: vscode.TextEditorDecorationType } = {};
|
||||
|
||||
private decorationUsesWholeLine: boolean = true; // Useful for debugging, set to false to see exact match ranges
|
||||
|
||||
private config?: interfaces.IExtensionConfiguration;
|
||||
private tracker: interfaces.IDocumentMergeConflictTracker;
|
||||
private updating = new Map<vscode.TextEditor, boolean>();
|
||||
|
||||
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.onDidChangeVisibleTextEditors((e) => {
|
||||
// Any of which could be new (not just the active one).
|
||||
e.forEach(e => 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('merge.currentContentBackground', 'editorOverviewRuler.currentContentForeground', config)
|
||||
);
|
||||
|
||||
this.decorations['incoming.content'] = vscode.window.createTextEditorDecorationType(
|
||||
this.generateBlockRenderOptions('merge.incomingContentBackground', 'editorOverviewRuler.incomingContentForeground', config)
|
||||
);
|
||||
|
||||
this.decorations['commonAncestors.content'] = vscode.window.createTextEditorDecorationType(
|
||||
this.generateBlockRenderOptions('merge.commonContentBackground', 'editorOverviewRuler.commonContentForeground', config)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.enableDecorations) {
|
||||
this.decorations['current.header'] = vscode.window.createTextEditorDecorationType({
|
||||
isWholeLine: this.decorationUsesWholeLine,
|
||||
backgroundColor: new vscode.ThemeColor('merge.currentHeaderBackground'),
|
||||
color: new vscode.ThemeColor('editor.foreground'),
|
||||
outlineStyle: 'solid',
|
||||
outlineWidth: '1pt',
|
||||
outlineColor: new vscode.ThemeColor('merge.border'),
|
||||
after: {
|
||||
contentText: ' ' + localize('currentChange', '(Current Change)'),
|
||||
color: new vscode.ThemeColor('descriptionForeground')
|
||||
}
|
||||
});
|
||||
|
||||
this.decorations['commonAncestors.header'] = vscode.window.createTextEditorDecorationType({
|
||||
isWholeLine: this.decorationUsesWholeLine,
|
||||
backgroundColor: new vscode.ThemeColor('merge.commonHeaderBackground'),
|
||||
color: new vscode.ThemeColor('editor.foreground'),
|
||||
outlineStyle: 'solid',
|
||||
outlineWidth: '1pt',
|
||||
outlineColor: new vscode.ThemeColor('merge.border')
|
||||
});
|
||||
|
||||
this.decorations['splitter'] = vscode.window.createTextEditorDecorationType({
|
||||
color: new vscode.ThemeColor('editor.foreground'),
|
||||
outlineStyle: 'solid',
|
||||
outlineWidth: '1pt',
|
||||
outlineColor: new vscode.ThemeColor('merge.border'),
|
||||
isWholeLine: this.decorationUsesWholeLine,
|
||||
});
|
||||
|
||||
this.decorations['incoming.header'] = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: new vscode.ThemeColor('merge.incomingHeaderBackground'),
|
||||
color: new vscode.ThemeColor('editor.foreground'),
|
||||
outlineStyle: 'solid',
|
||||
outlineWidth: '1pt',
|
||||
outlineColor: new vscode.ThemeColor('merge.border'),
|
||||
isWholeLine: this.decorationUsesWholeLine,
|
||||
after: {
|
||||
contentText: ' ' + localize('incomingChange', '(Incoming Change)'),
|
||||
color: new vscode.ThemeColor('descriptionForeground')
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
// TODO: Replace with Map<string, T>
|
||||
Object.keys(this.decorations).forEach(name => {
|
||||
this.decorations[name].dispose();
|
||||
});
|
||||
|
||||
this.decorations = {};
|
||||
}
|
||||
|
||||
private generateBlockRenderOptions(backgroundColor: string, overviewRulerColor: string, config: interfaces.IExtensionConfiguration): vscode.DecorationRenderOptions {
|
||||
|
||||
let renderOptions: vscode.DecorationRenderOptions = {};
|
||||
|
||||
if (config.enableDecorations) {
|
||||
renderOptions.backgroundColor = new vscode.ThemeColor(backgroundColor);
|
||||
renderOptions.isWholeLine = this.decorationUsesWholeLine;
|
||||
}
|
||||
|
||||
if (config.enableEditorOverview) {
|
||||
renderOptions.overviewRulerColor = new vscode.ThemeColor(overviewRulerColor);
|
||||
renderOptions.overviewRulerLane = vscode.OverviewRulerLane.Full;
|
||||
}
|
||||
|
||||
return renderOptions;
|
||||
}
|
||||
|
||||
private applyDecorationsFromEvent(eventDocument: vscode.TextDocument) {
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
if (editor.document === eventDocument) {
|
||||
// Attempt to apply
|
||||
this.applyDecorations(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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. (Cannot use this.tracker.isPending() because decorations are per editor.)
|
||||
if (this.updating.get(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.updating.set(editor, true);
|
||||
|
||||
let conflicts = await this.tracker.getConflicts(editor.document);
|
||||
if (vscode.window.visibleTextEditors.indexOf(editor) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.Range[] } = {};
|
||||
|
||||
let pushDecoration = (key: string, d: vscode.Range) => {
|
||||
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
|
||||
if (!conflict.current.decoratorContent.isEmpty) {
|
||||
pushDecoration('current.content', conflict.current.decoratorContent);
|
||||
}
|
||||
if (!conflict.incoming.decoratorContent.isEmpty) {
|
||||
pushDecoration('incoming.content', conflict.incoming.decoratorContent);
|
||||
}
|
||||
|
||||
conflict.commonAncestors.forEach(commonAncestorsRegion => {
|
||||
if (!commonAncestorsRegion.decoratorContent.isEmpty) {
|
||||
pushDecoration('commonAncestors.content', commonAncestorsRegion.decoratorContent);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.config!.enableDecorations) {
|
||||
pushDecoration('current.header', conflict.current.header);
|
||||
pushDecoration('splitter', conflict.splitter);
|
||||
pushDecoration('incoming.header', conflict.incoming.header);
|
||||
|
||||
conflict.commonAncestors.forEach(commonAncestorsRegion => {
|
||||
pushDecoration('commonAncestors.header', commonAncestorsRegion.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]);
|
||||
}
|
||||
});
|
||||
|
||||
} finally {
|
||||
this.updating.delete(editor);
|
||||
}
|
||||
}
|
||||
|
||||
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, []);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
67
lib/vscode/extensions/merge-conflict/src/services.ts
Normal file
67
lib/vscode/extensions/merge-conflict/src/services.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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(documentTracker),
|
||||
new CodeLensProvider(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 = [];
|
||||
}
|
||||
}
|
||||
|
7
lib/vscode/extensions/merge-conflict/src/typings/refs.d.ts
vendored
Normal file
7
lib/vscode/extensions/merge-conflict/src/typings/refs.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 in New Issue
Block a user