diff --git a/extensions/git/package.json b/extensions/git/package.json index ecca4d76e17..1b67c450a15 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -18,7 +18,8 @@ "scmSelectedProvider", "scmValidation", "tabInputTextMerge", - "timeline" + "timeline", + "contribMergeEditorMenus" ], "categories": [ "Other" @@ -609,6 +610,18 @@ "command": "git.openMergeEditor", "title": "%command.git.openMergeEditor%", "category": "Git" + }, + { + "command": "git.runGitMerge", + "title": "%command.git.runGitMerge%", + "category": "Git", + "enablement": "isMergeEditor" + }, + { + "command": "git.runGitMergeDiff3", + "title": "%command.git.runGitMergeDiff3%", + "category": "Git", + "enablement": "isMergeEditor" } ], "keybindings": [ @@ -1568,6 +1581,16 @@ "when": "config.git.enabled && !git.missing && !isInDiffEditor && !isMergeEditor && resource in git.mergeChanges" } ], + "mergeEditor/result/title": [ + { + "command": "git.runGitMerge", + "when": "isMergeEditor" + }, + { + "command": "git.runGitMergeDiff3", + "when": "isMergeEditor" + } + ], "scm/change/title": [ { "command": "git.stageChange", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index eaf627c3731..1aba43cb86a 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -104,6 +104,8 @@ "command.api.getRemoteSources": "Get Remote Sources", "command.git.acceptMerge": "Accept Merge", "command.git.openMergeEditor": "Open in Merge Editor", + "command.git.runGitMerge": "Compute Conflicts With Git", + "command.git.runGitMergeDiff3": "Compute Conflicts With Git (Diff3)", "config.enabled": "Whether git is enabled.", "config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows). This can also be an array of string values containing multiple paths to look up.", "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index bc5a7362ad5..9da99697a45 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1139,6 +1139,51 @@ export class CommandCenter { } } + @command('git.runGitMerge') + async runGitMergeNoDiff3(): Promise { + await this.runGitMerge(false); + } + + @command('git.runGitMergeDiff3') + async runGitMergeDiff3(): Promise { + await this.runGitMerge(true); + } + + private async runGitMerge(diff3: boolean): Promise { + const { activeTab } = window.tabGroups.activeTabGroup; + if (!activeTab) { + return; + } + + const input = activeTab.input; + if (!(input instanceof TabInputTextMerge)) { + return; + } + + const result = await this.git.mergeFile({ + basePath: input.base.fsPath, + input1Path: input.input1.fsPath, + input2Path: input.input2.fsPath, + diff3, + }); + + const doc = workspace.textDocuments.find(doc => doc.uri.toString() === input.result.toString()); + if (!doc) { + return; + } + const e = new WorkspaceEdit(); + + e.replace( + input.result, + new Range( + new Position(0, 0), + new Position(doc.lineCount, 0), + ), + result + ); + await workspace.applyEdit(e); + } + private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index bab143fdb4e..25c7ec7d858 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -649,6 +649,27 @@ export class Git { private log(output: string): void { this._onOutput.emit('log', output); } + + async mergeFile(options: { input1Path: string; input2Path: string; basePath: string; diff3?: boolean }): Promise { + const args = ['merge-file', '-p', options.input1Path, options.basePath, options.input2Path]; + if (options.diff3) { + args.push('--diff3'); + } else { + args.push('--no-diff3'); + } + + try { + const result = await this.exec('', args); + return result.stdout; + } catch (err) { + if (typeof err.stdout === 'string') { + // The merge had conflicts, stdout still contains the merged result (with conflict markers) + return err.stdout; + } else { + throw err; + } + } + } } export interface Commit { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 0a08af75abf..15384e1267f 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -167,6 +167,7 @@ export class MenuId { static readonly NewFile = new MenuId('NewFile'); static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar'); static readonly MergeInput2Toolbar = new MenuId('MergeToolbar2Toolbar'); + static readonly MergeInputResultToolbar = new MenuId('MergeToolbarResultToolbar'); /** * Create or reuse a `MenuId` with the given identifier diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 284e76467a7..ac6e8aa9c9f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -454,3 +454,51 @@ export class AcceptAllInput2 extends MergeEditorAction { viewModel.acceptAll(2); } } + +export class ResetToBaseAndAutoMergeCommand extends MergeEditorAction { + constructor() { + super({ + id: 'mergeEditor.resetResultToBaseAndAutoMerge', + category: mergeEditorCategory, + title: { + value: localize( + 'mergeEditor.resetResultToBaseAndAutoMerge', + 'Reset Result' + ), + original: 'Reset Result', + }, + shortTitle: localize('mergeEditor.resetResultToBaseAndAutoMerge.short', 'Reset'), + f1: true, + precondition: ctxIsMergeEditor, + menu: { id: MenuId.MergeInputResultToolbar } + }); + } + + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { + viewModel.model.resetResultToBaseAndAutoMerge(); + } +} + +export class ResetDirtyConflictsToBaseCommand extends MergeEditorAction { + constructor() { + super({ + id: 'mergeEditor.resetDirtyConflictsToBase', + category: mergeEditorCategory, + title: { + value: localize( + 'mergeEditor.resetDirtyConflictsToBase', + 'Reset Dirty Conflicts In Result To Base' + ), + original: 'Reset Dirty Conflicts In Result To Base', + }, + shortTitle: localize('mergeEditor.resetDirtyConflictsToBase.short', 'Reset Dirty Conflicts To Base'), + f1: true, + precondition: ctxIsMergeEditor, + menu: { id: MenuId.MergeInputResultToolbar } + }); + } + + override runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void { + viewModel.model.resetDirtyConflictsToBase(); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts new file mode 100644 index 00000000000..1c28237d1bf --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { Codicon } from 'vs/base/common/codicons'; +import { URI } from 'vs/base/common/uri'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { ctxIsMergeEditor, MergeEditorContents } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class MergeEditorCopyContentsToJSON extends Action2 { + constructor() { + super({ + id: 'merge.dev.copyContentsJson', + category: 'Merge Editor (Dev)', + title: { + value: localize( + 'merge.dev.copyState', + 'Copy Merge Editor State as JSON' + ), + original: 'Copy Merge Editor State as JSON', + }, + icon: Codicon.layoutCentered, + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + const clipboardService = accessor.get(IClipboardService); + const notificationService = accessor.get(INotificationService); + + if (!(activeEditorPane instanceof MergeEditor)) { + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.noActiveMergeEditor', "No active merge editor") + }); + return; + } + const model = activeEditorPane.model; + if (!model) { + return; + } + const contents: MergeEditorContents = { + languageId: model.resultTextModel.getLanguageId(), + base: model.base.getValue(), + input1: model.input1.textModel.getValue(), + input2: model.input2.textModel.getValue(), + result: model.resultTextModel.getValue(), + initialResult: model.getInitialResultValue(), + }; + const jsonStr = JSON.stringify(contents, undefined, 4); + clipboardService.writeText(jsonStr); + + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.successfullyCopiedMergeEditorContents', "Successfully copied merge editor state"), + }); + } +} + +export class MergeEditorSaveContentsToFolder extends Action2 { + constructor() { + super({ + id: 'merge.dev.saveContentsToFolder', + category: 'Merge Editor (Dev)', + title: { + value: localize( + 'merge.dev.saveContentsToFolder', + 'Save Merge Editor State to Folder' + ), + original: 'Save Merge Editor State to Folder', + }, + icon: Codicon.layoutCentered, + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + async run(accessor: ServicesAccessor) { + const { activeEditorPane } = accessor.get(IEditorService); + const notificationService = accessor.get(INotificationService); + const dialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const languageService = accessor.get(ILanguageService); + + if (!(activeEditorPane instanceof MergeEditor)) { + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.noActiveMergeEditor', "No active merge editor") + }); + return; + } + const model = activeEditorPane.model; + if (!model) { + return; + } + + const result = await dialogService.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: localize('mergeEditor.selectFolderToSaveTo', 'Select folder to save to') + }); + if (!result) { + return; + } + const targetDir = result[0]; + + const extension = languageService.getExtensions(model.resultTextModel.getLanguageId())[0] || ''; + + async function write(fileName: string, source: string) { + await fileService.writeFile(URI.joinPath(targetDir, fileName + extension), VSBuffer.fromString(source), {}); + } + + await Promise.all([ + write('base', model.base.getValue()), + write('input1', model.input1.textModel.getValue()), + write('input2', model.input2.textModel.getValue()), + write('result', model.resultTextModel.getValue()), + write('initialResult', model.getInitialResultValue()), + ]); + + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.successfullySavedMergeEditorContentsToFolder', "Successfully saved merge editor state to folder"), + }); + } +} + +export class MergeEditorLoadContentsFromFolder extends Action2 { + constructor() { + super({ + id: 'merge.dev.loadContentsFromFolder', + category: 'Merge Editor (Dev)', + title: { + value: localize( + 'merge.dev.loadContentsFromFolder', + 'Load Merge Editor State from Folder' + ), + original: 'Load Merge Editor State from Folder', + }, + icon: Codicon.layoutCentered, + f1: true + }); + } + + async run(accessor: ServicesAccessor, args?: { folderUri?: URI; resultState?: 'initial' | 'current' }) { + const dialogService = accessor.get(IFileDialogService); + const editorService = accessor.get(IEditorService); + const fileService = accessor.get(IFileService); + const quickInputService = accessor.get(IQuickInputService); + + if (!args) { + args = {}; + } + + let targetDir: URI; + if (!args.folderUri) { + const result = await dialogService.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: localize('mergeEditor.selectFolderToSaveTo', 'Select folder to save to') + }); + if (!result) { + return; + } + targetDir = result[0]; + } else { + targetDir = args.folderUri; + } + + const targetDirInfo = await fileService.resolve(targetDir); + + function findFile(name: string) { + return targetDirInfo.children!.find(c => c.name.startsWith(name))?.resource!; + } + + const shouldOpenInitial = await promptOpenInitial(quickInputService, args.resultState); + + const baseUri = findFile('base'); + const input1Uri = findFile('input1'); + const input2Uri = findFile('input2'); + const resultUri = findFile(shouldOpenInitial ? 'initialResult' : 'result'); + + const input: IResourceMergeEditorInput = { + base: { resource: baseUri }, + input1: { resource: input1Uri, label: 'Input 1', description: 'Input 1', detail: '(from file)' }, + input2: { resource: input2Uri, label: 'Input 2', description: 'Input 2', detail: '(from file)' }, + result: { resource: resultUri }, + }; + editorService.openEditor(input); + } +} + +async function promptOpenInitial(quickInputService: IQuickInputService, resultStateOverride?: 'initial' | 'current') { + if (resultStateOverride) { + return resultStateOverride === 'initial'; + } + const result = await quickInputService.pick([{ label: 'result', result: false }, { label: 'initial result', result: true }], { canPickMany: false }); + return result?.result; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index e5646d787b3..e66e56f859a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -11,7 +11,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, SetColumnLayout, SetMixedLayout, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { MergeEditorCopyContentsToJSON, MergeEditorSaveContentsToFolder, MergeEditorLoadContentsFromFolder } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditor, MergeEditorResolverContribution, MergeEditorOpenHandlerContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -65,6 +66,13 @@ registerAction2(CompareInput2WithBaseCommand); registerAction2(AcceptAllInput1); registerAction2(AcceptAllInput2); +registerAction2(ResetToBaseAndAutoMergeCommand); +registerAction2(ResetDirtyConflictsToBaseCommand); + +// Dev Commands +registerAction2(MergeEditorCopyContentsToJSON); +registerAction2(MergeEditorSaveContentsToFolder); +registerAction2(MergeEditorLoadContentsFromFolder); Registry .as(WorkbenchExtensions.Workbench) diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts index 9ff067339ea..de2e9e2c6df 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts @@ -42,7 +42,6 @@ export class LineEdits { constructor(public readonly edits: readonly LineRangeEdit[]) { } public apply(model: ITextModel): void { - model.pushStackElement(); model.pushEditOperations( null, this.edits.map((e) => { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index c165e579157..8b7304cbafc 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -187,7 +187,7 @@ export class MergeEditorModel extends EditorModel { if (options.resetUnknownOnInitialization) { this.onInitialized.then(() => { - this.resetUnknown(); + this.resetDirtyConflictsToBase(); }); } } @@ -217,20 +217,23 @@ export class MergeEditorModel extends EditorModel { } } - public resetUnknown(): void { + public resetDirtyConflictsToBase(): void { transaction(tx => { /** @description Reset Unknown Base Range States */ + this.resultTextModel.pushStackElement(); for (const range of this.modifiedBaseRanges.get()) { if (this.getState(range).get().conflicting) { - this.setState(range, ModifiedBaseRangeState.default, false, tx); + this.setState(range, ModifiedBaseRangeState.default, false, tx, false); } } + this.resultTextModel.pushStackElement(); }); } - public mergeNonConflictingDiffs(): void { + public acceptNonConflictingDiffs(): void { transaction((tx) => { /** @description Merge None Conflicting Diffs */ + this.resultTextModel.pushStackElement(); for (const m of this.modifiedBaseRanges.get()) { if (m.isConflicting) { continue; @@ -241,9 +244,11 @@ export class MergeEditorModel extends EditorModel { ? ModifiedBaseRangeState.default.withInput1(true) : ModifiedBaseRangeState.default.withInput2(true), true, - tx + tx, + false ); } + this.resultTextModel.pushStackElement(); }); } @@ -259,7 +264,8 @@ export class MergeEditorModel extends EditorModel { baseRange: ModifiedBaseRange, state: ModifiedBaseRangeState, markHandled: boolean, - transaction: ITransaction + transaction: ITransaction, + pushStackElement: boolean = false ): void { if (!this.isUpToDate.get()) { throw new BugIndicatingError('Cannot set state while updating'); @@ -282,7 +288,13 @@ export class MergeEditorModel extends EditorModel { existingState.set(effectiveState, transaction); if (edit) { + if (pushStackElement) { + this.resultTextModel.pushStackElement(); + } this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction); + if (pushStackElement) { + this.resultTextModel.pushStackElement(); + } } if (markHandled) { @@ -353,6 +365,12 @@ export class MergeEditorModel extends EditorModel { this.modelService.setMode(this.input2.textModel, language); this.modelService.setMode(this.resultTextModel, language); } + + public async resetResultToBaseAndAutoMerge() { + this.resultTextModel.setValue(this.base.getValue()); + await waitForState(this.state, state => state === MergeEditorModelState.upToDate); + this.acceptNonConflictingDiffs(); + } } export interface InputData { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index f38adcbc9b2..560d26d9e0c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -6,12 +6,18 @@ import { h, reset } from 'vs/base/browser/dom'; import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IAction } from 'vs/base/common/actions'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { InputData } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; @@ -110,3 +116,27 @@ export abstract class CodeEditorView extends Disposable { }); } } + +export class TitleMenu extends Disposable { + constructor( + menuId: MenuId, + targetHtmlElement: HTMLElement, + @IContextMenuService contextMenuService: IContextMenuService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + + const titleMenu = menuService.createMenu(menuId, contextKeyService); + const toolBar = new ToolBar(targetHtmlElement, contextMenuService); + const toolBarUpdate = () => { + const secondary: IAction[] = []; + createAndFillInActionBarActions(titleMenu, { renderShortTitle: true }, secondary); + toolBar.setActions([], secondary); + }; + this._store.add(toolBar); + this._store.add(titleMenu); + this._store.add(titleMenu.onDidChange(toolBarUpdate)); + toolBarUpdate(); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index 0b5ef74d836..586cbfbec7f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -5,19 +5,17 @@ import * as dom from 'vs/base/browser/dom'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; -import { autorun, derived, IObservable, ISettableObservable, ITransaction, transaction, observableValue } from 'vs/base/common/observable'; +import { autorun, derived, IObservable, ISettableObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; import { noBreakWhitespace } from 'vs/base/common/strings'; import { isDefined } from 'vs/base/common/types'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; import { localize } from 'vs/nls'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -28,7 +26,7 @@ import { InputState, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEd import { applyObservableDecorations, setFields } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../editorGutter'; -import { CodeEditorView } from './codeEditorView'; +import { CodeEditorView, TitleMenu } from './codeEditorView'; export class InputCodeEditorView extends CodeEditorView { private readonly decorations = derived(`input${this.inputNumber}.decorations`, reader => { @@ -256,7 +254,6 @@ export class InputCodeEditorView extends CodeEditorView { constructor( public readonly inputNumber: 1 | 2, - titleMenuId: MenuId, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService, @@ -276,18 +273,13 @@ export class InputCodeEditorView extends CodeEditorView { }) ); - // title menu - const titleMenu = menuService.createMenu(titleMenuId, contextKeyService); - const toolBar = new ToolBar(this.htmlElements.toolbar, contextMenuService); - const toolBarUpdate = () => { - const secondary: IAction[] = []; - createAndFillInActionBarActions(titleMenu, { renderShortTitle: true }, secondary); - toolBar.setActions([], secondary); - }; - this._store.add(toolBar); - this._store.add(titleMenu); - this._store.add(titleMenu.onDidChange(toolBarUpdate)); - toolBarUpdate(); + this._register( + instantiationService.createInstance( + TitleMenu, + inputNumber === 1 ? MenuId.MergeInput1Toolbar : MenuId.MergeInput2Toolbar, + this.htmlElements.toolbar + ) + ); } protected override getEditorContributions(): IEditorContributionDescription[] | undefined { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts index 0632fc504f2..36c5a093c4d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts @@ -9,6 +9,7 @@ import { toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived } from 'vs/base/common/observable'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MergeMarkersController } from 'vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController'; @@ -17,7 +18,7 @@ import { applyObservableDecorations, join } from 'vs/workbench/contrib/mergeEdit import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; import { EditorGutter } from 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter'; import { ctxIsMergeResultEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; -import { CodeEditorView } from './codeEditorView'; +import { CodeEditorView, TitleMenu } from './codeEditorView'; export class ResultCodeEditorView extends CodeEditorView { private readonly decorations = derived('result.decorations', reader => { @@ -112,7 +113,7 @@ export class ResultCodeEditorView extends CodeEditorView { }); constructor( - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, ) { super(instantiationService); @@ -160,5 +161,13 @@ export class ResultCodeEditorView extends CodeEditorView { ); })); + + this._register( + instantiationService.createInstance( + TitleMenu, + MenuId.MergeInputResultToolbar, + this.htmlElements.toolbar + ) + ); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 758d2966ae1..b8a01681d4c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -22,7 +22,6 @@ import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/ed import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions, ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -42,7 +41,7 @@ import { DocumentMapping, getOppositeDirection, MappingDirection } from 'vs/work import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { deepMerge, ReentrancyBarrier, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; -import { ctxMergeBaseUri, ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeResultUri, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { ctxIsMergeEditor, ctxMergeBaseUri, ctxMergeEditorLayout, ctxMergeResultUri, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -85,8 +84,8 @@ export class MergeEditor extends AbstractTextEditor { private readonly _sessionDisposables = new DisposableStore(); private _grid!: Grid; - private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1, MenuId.MergeInput1Toolbar)); - private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, MenuId.MergeInput2Toolbar)); + private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1)); + private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2)); private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView)); private readonly _layoutMode: MergeEditorLayout; diff --git a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts index 56da17d7333..a88895826d2 100644 --- a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts @@ -13,3 +13,12 @@ export const ctxIsMergeResultEditor = new RawContextKey('isMergeResultE export const ctxMergeEditorLayout = new RawContextKey('mergeEditorLayout', 'mixed', { type: 'string', description: localize('editorLayout', 'The layout mode of a merge editor') }); export const ctxMergeBaseUri = new RawContextKey('mergeEditorBaseUri', '', { type: 'string', description: localize('baseUri', 'The uri of the baser of a merge editor') }); export const ctxMergeResultUri = new RawContextKey('mergeEditorResultUri', '', { type: 'string', description: localize('resultUri', 'The uri of the result of a merge editor') }); + +export interface MergeEditorContents { + languageId: string; + base: string; + input1: string; + input2: string; + result: string; + initialResult?: string; +} diff --git a/src/vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands.ts b/src/vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands.ts index 5d66e435319..7f89c306c20 100644 --- a/src/vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands.ts +++ b/src/vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands.ts @@ -11,78 +11,14 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; -import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; -import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { MergeEditorContents } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -interface MergeEditorContents { - languageId: string; - base: string; - input1: string; - input2: string; - result: string; - initialResult?: string; -} - -export class MergeEditorCopyContentsToJSON extends Action2 { - constructor() { - super({ - id: 'merge.dev.copyContentsJson', - category: 'Merge Editor (Dev)', - title: { - value: localize( - 'merge.dev.copyState', - 'Copy Merge Editor State as JSON' - ), - original: 'Copy Merge Editor State as JSON', - }, - icon: Codicon.layoutCentered, - f1: true, - precondition: ctxIsMergeEditor, - }); - } - - run(accessor: ServicesAccessor): void { - const { activeEditorPane } = accessor.get(IEditorService); - const clipboardService = accessor.get(IClipboardService); - const notificationService = accessor.get(INotificationService); - - if (!(activeEditorPane instanceof MergeEditor)) { - notificationService.info({ - name: localize('mergeEditor.name', 'Merge Editor'), - message: localize('mergeEditor.noActiveMergeEditor', "No active merge editor") - }); - return; - } - const model = activeEditorPane.model; - if (!model) { - return; - } - const contents: MergeEditorContents = { - languageId: model.resultTextModel.getLanguageId(), - base: model.base.getValue(), - input1: model.input1.textModel.getValue(), - input2: model.input2.textModel.getValue(), - result: model.resultTextModel.getValue(), - initialResult: model.getInitialResultValue(), - }; - const jsonStr = JSON.stringify(contents, undefined, 4); - clipboardService.writeText(jsonStr); - - notificationService.info({ - name: localize('mergeEditor.name', 'Merge Editor'), - message: localize('mergeEditor.successfullyCopiedMergeEditorContents', "Successfully copied merge editor state"), - }); - } -} - export class MergeEditorOpenContentsFromJSON extends Action2 { constructor() { super({ @@ -163,141 +99,6 @@ export class MergeEditorOpenContentsFromJSON extends Action2 { } } -export class MergeEditorSaveContentsToFolder extends Action2 { - constructor() { - super({ - id: 'merge.dev.saveContentsToFolder', - category: 'Merge Editor (Dev)', - title: { - value: localize( - 'merge.dev.saveContentsToFolder', - 'Save Merge Editor State to Folder' - ), - original: 'Save Merge Editor State to Folder', - }, - icon: Codicon.layoutCentered, - f1: true, - precondition: ctxIsMergeEditor, - }); - } - - async run(accessor: ServicesAccessor) { - const { activeEditorPane } = accessor.get(IEditorService); - const notificationService = accessor.get(INotificationService); - const dialogService = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const languageService = accessor.get(ILanguageService); - - if (!(activeEditorPane instanceof MergeEditor)) { - notificationService.info({ - name: localize('mergeEditor.name', 'Merge Editor'), - message: localize('mergeEditor.noActiveMergeEditor', "No active merge editor") - }); - return; - } - const model = activeEditorPane.model; - if (!model) { - return; - } - - const result = await dialogService.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - title: localize('mergeEditor.selectFolderToSaveTo', 'Select folder to save to') - }); - if (!result) { - return; - } - const targetDir = result[0]; - - const extension = languageService.getExtensions(model.resultTextModel.getLanguageId())[0] || ''; - - async function write(fileName: string, source: string) { - await fileService.writeFile(URI.joinPath(targetDir, fileName + extension), VSBuffer.fromString(source), {}); - } - - await Promise.all([ - write('base', model.base.getValue()), - write('input1', model.input1.textModel.getValue()), - write('input2', model.input2.textModel.getValue()), - write('result', model.resultTextModel.getValue()), - write('initialResult', model.getInitialResultValue()), - ]); - - notificationService.info({ - name: localize('mergeEditor.name', 'Merge Editor'), - message: localize('mergeEditor.successfullySavedMergeEditorContentsToFolder', "Successfully saved merge editor state to folder"), - }); - } -} - -export class MergeEditorLoadContentsFromFolder extends Action2 { - constructor() { - super({ - id: 'merge.dev.loadContentsFromFolder', - category: 'Merge Editor (Dev)', - title: { - value: localize( - 'merge.dev.loadContentsFromFolder', - 'Load Merge Editor State from Folder' - ), - original: 'Load Merge Editor State from Folder', - }, - icon: Codicon.layoutCentered, - f1: true - }); - } - - async run(accessor: ServicesAccessor, args?: { folderUri?: URI; resultState?: 'initial' | 'current' }) { - const dialogService = accessor.get(IFileDialogService); - const editorService = accessor.get(IEditorService); - const fileService = accessor.get(IFileService); - const quickInputService = accessor.get(IQuickInputService); - - if (!args) { - args = {}; - } - - let targetDir: URI; - if (!args.folderUri) { - const result = await dialogService.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - title: localize('mergeEditor.selectFolderToSaveTo', 'Select folder to save to') - }); - if (!result) { - return; - } - targetDir = result[0]; - } else { - targetDir = args.folderUri; - } - - const targetDirInfo = await fileService.resolve(targetDir); - - function findFile(name: string) { - return targetDirInfo.children!.find(c => c.name.startsWith(name))?.resource!; - } - - const shouldOpenInitial = await promptOpenInitial(quickInputService, args.resultState); - - const baseUri = findFile('base'); - const input1Uri = findFile('input1'); - const input2Uri = findFile('input2'); - const resultUri = findFile(shouldOpenInitial ? 'initialResult' : 'result'); - - const input: IResourceMergeEditorInput = { - base: { resource: baseUri }, - input1: { resource: input1Uri, label: 'Input 1', description: 'Input 1', detail: '(from file)' }, - input2: { resource: input2Uri, label: 'Input 2', description: 'Input 2', detail: '(from file)' }, - result: { resource: resultUri }, - }; - editorService.openEditor(input); - } -} - async function promptOpenInitial(quickInputService: IQuickInputService, resultStateOverride?: 'initial' | 'current') { if (resultStateOverride) { return resultStateOverride === 'initial'; diff --git a/src/vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution.ts index dca92757f29..1ee975754b1 100644 --- a/src/vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/electron-sandbox/mergeEditor.contribution.ts @@ -4,10 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { MergeEditorCopyContentsToJSON, MergeEditorLoadContentsFromFolder, MergeEditorOpenContentsFromJSON, MergeEditorSaveContentsToFolder } from 'vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands'; +import { MergeEditorOpenContentsFromJSON } from 'vs/workbench/contrib/mergeEditor/electron-sandbox/devCommands'; // Dev Commands -registerAction2(MergeEditorCopyContentsToJSON); registerAction2(MergeEditorOpenContentsFromJSON); -registerAction2(MergeEditorSaveContentsToFolder); -registerAction2(MergeEditorLoadContentsFromFolder); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 7ea40399d75..aeb6494aba8 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -291,7 +291,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('webview.context', "The webview context menu"), proposed: 'contribWebviewContext' }, - + { + key: 'mergeEditor/result/title', + id: MenuId.MergeInputResultToolbar, + description: localize('menus.mergeEditorResult', "The result toolbar of the merge editor"), + proposed: 'contribMergeEditorMenus' + }, ]; namespace schema { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 2754bd2327d..6bedcd03a63 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -13,6 +13,7 @@ export const allApiProposals = Object.freeze({ contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + contribMergeEditorMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', contribViewSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewSize.d.ts', diff --git a/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts b/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts new file mode 100644 index 00000000000..6a6e5c22d33 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for `mergeEditor/*` menus