Path: blob/main/extensions/merge-conflict/src/commandHandler.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as vscode from 'vscode';5import * as interfaces from './interfaces';6import ContentProvider from './contentProvider';78interface IDocumentMergeConflictNavigationResults {9canNavigate: boolean;10conflict?: interfaces.IDocumentMergeConflict;11}1213enum NavigationDirection {14Forwards,15Backwards16}1718export default class CommandHandler implements vscode.Disposable {1920private disposables: vscode.Disposable[] = [];21private tracker: interfaces.IDocumentMergeConflictTracker;2223constructor(trackerService: interfaces.IDocumentMergeConflictTrackerService) {24this.tracker = trackerService.createTracker('commands');25}2627begin() {28this.disposables.push(29this.registerTextEditorCommand('merge-conflict.accept.current', this.acceptCurrent),30this.registerTextEditorCommand('merge-conflict.accept.incoming', this.acceptIncoming),31this.registerTextEditorCommand('merge-conflict.accept.selection', this.acceptSelection),32this.registerTextEditorCommand('merge-conflict.accept.both', this.acceptBoth),33this.registerTextEditorCommand('merge-conflict.accept.all-current', this.acceptAllCurrent, this.acceptAllCurrentResources),34this.registerTextEditorCommand('merge-conflict.accept.all-incoming', this.acceptAllIncoming, this.acceptAllIncomingResources),35this.registerTextEditorCommand('merge-conflict.accept.all-both', this.acceptAllBoth),36this.registerTextEditorCommand('merge-conflict.next', this.navigateNext),37this.registerTextEditorCommand('merge-conflict.previous', this.navigatePrevious),38this.registerTextEditorCommand('merge-conflict.compare', this.compare)39);40}4142private registerTextEditorCommand(command: string, cb: (editor: vscode.TextEditor, ...args: any[]) => Promise<void>, resourceCB?: (uris: vscode.Uri[]) => Promise<void>) {43return vscode.commands.registerCommand(command, (...args) => {44if (resourceCB && args.length && args.every(arg => arg && arg.resourceUri)) {45return resourceCB.call(this, args.map(arg => arg.resourceUri));46}47const editor = vscode.window.activeTextEditor;48return editor && cb.call(this, editor, ...args);49});50}5152acceptCurrent(editor: vscode.TextEditor, ...args: any[]): Promise<void> {53return this.accept(interfaces.CommitType.Current, editor, ...args);54}5556acceptIncoming(editor: vscode.TextEditor, ...args: any[]): Promise<void> {57return this.accept(interfaces.CommitType.Incoming, editor, ...args);58}5960acceptBoth(editor: vscode.TextEditor, ...args: any[]): Promise<void> {61return this.accept(interfaces.CommitType.Both, editor, ...args);62}6364acceptAllCurrent(editor: vscode.TextEditor): Promise<void> {65return this.acceptAll(interfaces.CommitType.Current, editor);66}6768acceptAllIncoming(editor: vscode.TextEditor): Promise<void> {69return this.acceptAll(interfaces.CommitType.Incoming, editor);70}7172acceptAllCurrentResources(resources: vscode.Uri[]): Promise<void> {73return this.acceptAllResources(interfaces.CommitType.Current, resources);74}7576acceptAllIncomingResources(resources: vscode.Uri[]): Promise<void> {77return this.acceptAllResources(interfaces.CommitType.Incoming, resources);78}7980acceptAllBoth(editor: vscode.TextEditor): Promise<void> {81return this.acceptAll(interfaces.CommitType.Both, editor);82}8384async compare(editor: vscode.TextEditor, conflict: interfaces.IDocumentMergeConflict | null) {8586// No conflict, command executed from command palette87if (!conflict) {88conflict = await this.findConflictContainingSelection(editor);8990// Still failed to find conflict, warn the user and exit91if (!conflict) {92vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));93return;94}95}9697const conflicts = await this.tracker.getConflicts(editor.document);9899// Still failed to find conflict, warn the user and exit100if (!conflicts) {101vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));102return;103}104105const scheme = editor.document.uri.scheme;106let range = conflict.current.content;107const leftRanges = conflicts.map(conflict => [conflict.current.content, conflict.range]);108const rightRanges = conflicts.map(conflict => [conflict.incoming.content, conflict.range]);109110const leftUri = editor.document.uri.with({111scheme: ContentProvider.scheme,112query: JSON.stringify({ scheme, range: range, ranges: leftRanges })113});114115116range = conflict.incoming.content;117const rightUri = leftUri.with({ query: JSON.stringify({ scheme, ranges: rightRanges }) });118119let mergeConflictLineOffsets = 0;120for (const nextconflict of conflicts) {121if (nextconflict.range.isEqual(conflict.range)) {122break;123} else {124mergeConflictLineOffsets += (nextconflict.range.end.line - nextconflict.range.start.line) - (nextconflict.incoming.content.end.line - nextconflict.incoming.content.start.line);125}126}127const selection = new vscode.Range(128conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character,129conflict.range.start.line - mergeConflictLineOffsets, conflict.range.start.character130);131132const docPath = editor.document.uri.path;133const fileName = docPath.substring(docPath.lastIndexOf('/') + 1); // avoid NodeJS path to keep browser webpack small134const title = vscode.l10n.t("{0}: Current Changes ↔ Incoming Changes", fileName);135const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');136const openToTheSide = mergeConflictConfig.get<string>('diffViewPosition');137const opts: vscode.TextDocumentShowOptions = {138viewColumn: openToTheSide === 'Beside' ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,139selection140};141142if (openToTheSide === 'Below') {143await vscode.commands.executeCommand('workbench.action.newGroupBelow');144}145146await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, title, opts);147}148149navigateNext(editor: vscode.TextEditor): Promise<void> {150return this.navigate(editor, NavigationDirection.Forwards);151}152153navigatePrevious(editor: vscode.TextEditor): Promise<void> {154return this.navigate(editor, NavigationDirection.Backwards);155}156157async acceptSelection(editor: vscode.TextEditor): Promise<void> {158const conflict = await this.findConflictContainingSelection(editor);159160if (!conflict) {161vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));162return;163}164165let typeToAccept: interfaces.CommitType;166let tokenAfterCurrentBlock: vscode.Range = conflict.splitter;167168if (conflict.commonAncestors.length > 0) {169tokenAfterCurrentBlock = conflict.commonAncestors[0].header;170}171172// Figure out if the cursor is in current or incoming, we do this by seeing if173// the active position is before or after the range of the splitter or common174// ancestors marker. We can use this trick as the previous check in175// findConflictByActiveSelection will ensure it's within the conflict range, so176// we don't falsely identify "current" or "incoming" if outside of a conflict range.177if (editor.selection.active.isBefore(tokenAfterCurrentBlock.start)) {178typeToAccept = interfaces.CommitType.Current;179}180else if (editor.selection.active.isAfter(conflict.splitter.end)) {181typeToAccept = interfaces.CommitType.Incoming;182}183else if (editor.selection.active.isBefore(conflict.splitter.start)) {184vscode.window.showWarningMessage(vscode.l10n.t('Editor cursor is within the common ancestors block, please move it to either the "current" or "incoming" block'));185return;186}187else {188vscode.window.showWarningMessage(vscode.l10n.t('Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block'));189return;190}191192this.tracker.forget(editor.document);193conflict.commitEdit(typeToAccept, editor);194}195196dispose() {197this.disposables.forEach(disposable => disposable.dispose());198this.disposables = [];199}200201private async navigate(editor: vscode.TextEditor, direction: NavigationDirection): Promise<void> {202const navigationResult = await this.findConflictForNavigation(editor, direction);203204if (!navigationResult) {205// Check for autoNavigateNextConflict, if it's enabled(which indicating no conflict remain), then do not show warning206const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');207if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {208return;209}210vscode.window.showWarningMessage(vscode.l10n.t("No merge conflicts found in this file"));211return;212}213else if (!navigationResult.canNavigate) {214vscode.window.showWarningMessage(vscode.l10n.t("No other merge conflicts within this file"));215return;216}217else if (!navigationResult.conflict) {218// TODO: Show error message?219return;220}221222// Move the selection to the first line of the conflict223editor.selection = new vscode.Selection(navigationResult.conflict.range.start, navigationResult.conflict.range.start);224editor.revealRange(navigationResult.conflict.range, vscode.TextEditorRevealType.Default);225}226227private async accept(type: interfaces.CommitType, editor: vscode.TextEditor, ...args: any[]): Promise<void> {228229let conflict: interfaces.IDocumentMergeConflict | null;230231// If launched with known context, take the conflict from that232if (args[0] === 'known-conflict') {233conflict = args[1];234}235else {236// Attempt to find a conflict that matches the current cursor position237conflict = await this.findConflictContainingSelection(editor);238}239240if (!conflict) {241vscode.window.showWarningMessage(vscode.l10n.t("Editor cursor is not within a merge conflict"));242return;243}244245// Tracker can forget as we know we are going to do an edit246this.tracker.forget(editor.document);247conflict.commitEdit(type, editor);248249// navigate to the next merge conflict250const mergeConflictConfig = vscode.workspace.getConfiguration('merge-conflict');251if (mergeConflictConfig.get<boolean>('autoNavigateNextConflict.enabled')) {252this.navigateNext(editor);253}254255}256257private async acceptAll(type: interfaces.CommitType, editor: vscode.TextEditor): Promise<void> {258const conflicts = await this.tracker.getConflicts(editor.document);259260if (!conflicts || conflicts.length === 0) {261vscode.window.showWarningMessage(vscode.l10n.t("No merge conflicts found in this file"));262return;263}264265// For get the current state of the document, as we know we are doing to do a large edit266this.tracker.forget(editor.document);267268// Apply all changes as one edit269await editor.edit((edit) => conflicts.forEach(conflict => {270conflict.applyEdit(type, editor.document, edit);271}));272}273274private async acceptAllResources(type: interfaces.CommitType, resources: vscode.Uri[]): Promise<void> {275const documents = await Promise.all(resources.map(resource => vscode.workspace.openTextDocument(resource)));276const edit = new vscode.WorkspaceEdit();277for (const document of documents) {278const conflicts = await this.tracker.getConflicts(document);279280if (!conflicts || conflicts.length === 0) {281continue;282}283284// For get the current state of the document, as we know we are doing to do a large edit285this.tracker.forget(document);286287// Apply all changes as one edit288conflicts.forEach(conflict => {289conflict.applyEdit(type, document, { replace: (range, newText) => edit.replace(document.uri, range, newText) });290});291}292vscode.workspace.applyEdit(edit);293}294295private async findConflictContainingSelection(editor: vscode.TextEditor, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<interfaces.IDocumentMergeConflict | null> {296297if (!conflicts) {298conflicts = await this.tracker.getConflicts(editor.document);299}300301if (!conflicts || conflicts.length === 0) {302return null;303}304305for (const conflict of conflicts) {306if (conflict.range.contains(editor.selection.active)) {307return conflict;308}309}310311return null;312}313314private async findConflictForNavigation(editor: vscode.TextEditor, direction: NavigationDirection, conflicts?: interfaces.IDocumentMergeConflict[]): Promise<IDocumentMergeConflictNavigationResults | null> {315if (!conflicts) {316conflicts = await this.tracker.getConflicts(editor.document);317}318319if (!conflicts || conflicts.length === 0) {320return null;321}322323const selection = editor.selection.active;324if (conflicts.length === 1) {325if (conflicts[0].range.contains(selection)) {326return {327canNavigate: false328};329}330331return {332canNavigate: true,333conflict: conflicts[0]334};335}336337let predicate: (_conflict: any) => boolean;338let fallback: () => interfaces.IDocumentMergeConflict;339let scanOrder: interfaces.IDocumentMergeConflict[];340341if (direction === NavigationDirection.Forwards) {342predicate = (conflict) => selection.isBefore(conflict.range.start);343fallback = () => conflicts![0];344scanOrder = conflicts;345} else if (direction === NavigationDirection.Backwards) {346predicate = (conflict) => selection.isAfter(conflict.range.start);347fallback = () => conflicts![conflicts!.length - 1];348scanOrder = conflicts.slice().reverse();349} else {350throw new Error(`Unsupported direction ${direction}`);351}352353for (const conflict of scanOrder) {354if (predicate(conflict) && !conflict.range.contains(selection)) {355return {356canNavigate: true,357conflict: conflict358};359}360}361362// Went all the way to the end, return the head363return {364canNavigate: true,365conflict: fallback()366};367}368}369370371