diff --git a/extensions/git/package.json b/extensions/git/package.json index 27811e8e93e..8a0a8cf7ed1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -16,6 +16,7 @@ "scmActionButton", "scmSelectedProvider", "scmValidation", + "tabInputTextMerge", "timeline" ], "categories": [ diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 0e3e19792cc..7ebe8bdc606 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; @@ -1103,21 +1103,26 @@ export class CommandCenter { return; } + const { activeTab } = window.tabGroups.activeTabGroup; + if (!activeTab) { + return; + } + + // make sure to save the merged document const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); if (!doc) { console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't match a document`); return; } + if (doc.isDirty) { + await doc.save(); + } - await doc.save(); - - // TODO@jrieken there isn't a `TabInputTextMerge` instance yet, till now the merge editor - // uses the `TabInputText` for the out-resource and we use that to identify and CLOSE the tab - // see https://github.com/microsoft/vscode/issues/153213 - const { activeTab } = window.tabGroups.activeTabGroup; + // find the merge editor tabs for the resource in question and close them all let didCloseTab = false; - if (activeTab && activeTab?.input instanceof TabInputText && activeTab.input.uri.toString() === uri.toString()) { - didCloseTab = await window.tabGroups.close(activeTab, true); + const mergeEditorTabs = window.tabGroups.all.map(group => group.tabs.filter(tab => tab.input instanceof TabInputTextMerge && tab.input.result.toString() === uri.toString())).flat(); + if (mergeEditorTabs.includes(activeTab)) { + didCloseTab = await window.tabGroups.close(mergeEditorTabs, true); } // Only stage if the merge editor has been successfully closed. That means all conflicts have been diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 13997275056..c62c25401f2 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -14,7 +14,7 @@ "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts", "../../src/vscode-dts/vscode.proposed.scmValidation.d.ts", - "../../src/vscode-dts/vscode.proposed.tabs.d.ts", + "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", "../types/lib.textEncoder.d.ts" ] diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index c410ad77dc0..3b9b4dec1fe 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -23,6 +23,7 @@ import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEdit import { isEqual } from 'vs/base/common/resources'; import { isGroupEditorMoveEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; interface TabInfo { tab: IEditorTabDto; @@ -91,6 +92,16 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { private _editorInputToDto(editor: EditorInput): AnyInputDto { + if (editor instanceof MergeEditorInput) { + return { + kind: TabInputKind.TextMergeInput, + base: editor.base, + input1: editor.input1.uri, + input2: editor.input2.uri, + result: editor.resource + }; + } + if (editor instanceof AbstractTextResourceEditorInput) { return { kind: TabInputKind.TextInput, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 46325230f3c..2be02ab4797 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1342,6 +1342,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InputBoxValidationSeverity: extHostTypes.InputBoxValidationSeverity, TabInputText: extHostTypes.TextTabInput, TabInputTextDiff: extHostTypes.TextDiffTabInput, + TabInputTextMerge: extHostTypes.TextMergeTabInput, TabInputCustom: extHostTypes.CustomEditorTabInput, TabInputNotebook: extHostTypes.NotebookEditorTabInput, TabInputNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8df19f84b76..16f0fd0ac13 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -620,6 +620,7 @@ export const enum TabInputKind { UnknownInput, TextInput, TextDiffInput, + TextMergeInput, NotebookInput, NotebookDiffInput, CustomEditorInput, @@ -650,6 +651,14 @@ export interface TextDiffInputDto { modified: UriComponents; } +export interface TextMergeInputDto { + kind: TabInputKind.TextMergeInput; + base: UriComponents; + input1: UriComponents; + input2: UriComponents; + result: UriComponents; +} + export interface NotebookInputDto { kind: TabInputKind.NotebookInput; notebookType: string; @@ -684,7 +693,7 @@ export interface TabInputDto { kind: TabInputKind.TerminalEditorInput; } -export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | TabInputDto; +export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | TabInputDto; export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 1df76fd88c7..3282c7cc746 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -9,7 +9,7 @@ import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { assertIsDefined } from 'vs/base/common/types'; import { diffSets } from 'vs/base/common/collections'; @@ -84,6 +84,8 @@ class ExtHostEditorTab { return new TextTabInput(URI.revive(this._dto.input.uri)); case TabInputKind.TextDiffInput: return new TextDiffTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified)); + case TabInputKind.TextMergeInput: + return new TextMergeTabInput(URI.revive(this._dto.input.base), URI.revive(this._dto.input.input1), URI.revive(this._dto.input.input2), URI.revive(this._dto.input.result)); case TabInputKind.CustomEditorInput: return new CustomEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.viewType); case TabInputKind.WebviewEditorInput: @@ -110,7 +112,7 @@ class ExtHostEditorTabGroup { private _activeTabId: string = ''; private _activeGroupIdGetter: () => number | undefined; - constructor(dto: IEditorTabGroupDto, proxy: MainThreadEditorTabsShape, activeGroupIdGetter: () => number | undefined) { + constructor(dto: IEditorTabGroupDto, activeGroupIdGetter: () => number | undefined) { this._dto = dto; this._activeGroupIdGetter = activeGroupIdGetter; // Construct all tabs from the given dto @@ -284,7 +286,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { this._extHostTabGroups = tabGroups.map(tabGroup => { - const group = new ExtHostEditorTabGroup(tabGroup, this._proxy, () => this._activeGroupId); + const group = new ExtHostEditorTabGroup(tabGroup, () => this._activeGroupId); if (diff.added.includes(group.groupId)) { opened.push(group.apiObject); } else { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2f5af73ddf2..987ca4c4a30 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3797,6 +3797,10 @@ export class TextDiffTabInput { constructor(readonly original: URI, readonly modified: URI) { } } +export class TextMergeTabInput { + constructor(readonly base: URI, readonly input1: URI, readonly input2: URI, readonly result: URI) { } +} + export class CustomEditorTabInput { constructor(readonly uri: URI, readonly viewType: string) { } } diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 4c2501f4692..39bf2306faa 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -10,7 +10,7 @@ import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { TextMergeTabInput, TextTabInput } from 'vs/workbench/api/common/extHostTypes'; suite('ExtHostEditorTabs', function () { @@ -209,6 +209,37 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, first); }); + test('TextMergeTabInput surfaces in the UI', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab: IEditorTabDto = createTabDto({ + input: { + kind: TabInputKind.TextMergeInput, + base: URI.from({ scheme: 'test', path: 'base' }), + input1: URI.from({ scheme: 'test', path: 'input1' }), + input2: URI.from({ scheme: 'test', path: 'input2' }), + result: URI.from({ scheme: 'test', path: 'result' }), + } + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; + assert.ok(first.activeTab); + assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); + assert.ok(first.activeTab.input instanceof TextMergeTabInput); + }); + test('Ensure reference stability', function () { const extHostEditorTabs = new ExtHostEditorTabs( diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 012e90c9384..ca5260d65af 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -52,6 +52,7 @@ export const allApiProposals = Object.freeze({ scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', snippetWorkspaceEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts', + tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', diff --git a/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts b/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts new file mode 100644 index 00000000000..da95fd1d35b --- /dev/null +++ b/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// https://github.com/microsoft/vscode/issues/153213 + +declare module 'vscode' { + + export class TabInputTextMerge { + + readonly base: Uri; + readonly input1: Uri; + readonly input2: Uri; + readonly result: Uri; + + constructor(base: Uri, input1: Uri, input2: Uri, result: Uri); + } + + export interface Tab { + + readonly input: TabInputText | TabInputTextDiff | TabInputTextMerge | TabInputCustom | TabInputWebview | TabInputNotebook | TabInputNotebookDiff | TabInputTerminal | unknown; + + } +}