diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index a0fe9e9c730..f7abfd1256c 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -41,7 +41,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); - // Queue all events that arrive on the same event loop and then send them as a batch + // Main listener which responds to events from the editor service this._dispoables.add(editorService.onDidEditorsChange((event) => this._updateTabsModel(event))); this._editorGroupsService.whenReady.then(() => this._createTabsModel()); } diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 9fb18d8b86e..a468e720062 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -5,160 +5,202 @@ import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabKind } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ViewColumn } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -export interface IEditorTab { - label: string; - viewColumn: ViewColumn; - resource: vscode.Uri | undefined; - viewType: string | undefined; - isActive: boolean; - isPinned: boolean; - kind: TabKind; - isDirty: boolean; - additionalResourcesAndViewTypes: { resource: vscode.Uri | undefined; viewType: string | undefined }[]; - move(index: number, viewColumn: ViewColumn): Promise; - close(preserveFocus: boolean): Promise; -} - -export interface IEditorTabGroup { - isActive: boolean; - viewColumn: ViewColumn; - activeTab: IEditorTab | undefined; - tabs: IEditorTab[]; -} - -export interface IEditorTabGroups { - groups: IEditorTabGroup[]; - activeTabGroup: IEditorTabGroup | undefined; - readonly onDidChangeTabGroup: Event; - readonly onDidChangeActiveTabGroup: Event; -} export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { readonly _serviceBrand: undefined; - tabGroups: IEditorTabGroups; + tabGroups: vscode.TabGroups; } export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); +class ExtHostEditorTabGroup { + + private _apiObject: vscode.TabGroup | undefined; + private _dto: IEditorTabGroupDto; + private _tabs: ExtHostEditorTab[] = []; + // private _activeTabId: string = ''; + + constructor(dto: IEditorTabGroupDto, proxy: MainThreadEditorTabsShape) { + this._dto = dto; + // Construct all tabs from the given dto + this._tabs = dto.tabs.map(tab => new ExtHostEditorTab(tab, proxy)); + } + + get apiObject(): vscode.TabGroup { + // Don't want to lose reference to parent `this` in the getters + const that = this; + if (!this._apiObject) { + this._apiObject = Object.freeze({ + get isActive() { + return that._dto.isActive; + }, + get viewColumn() { + return typeConverters.ViewColumn.to(that._dto.viewColumn); + }, + get activeTab() { + return that._tabs.find(tab => that._dto.activeTab?.id === tab.tabId)?.apiObject; + // return that._tabs.find(tab => tab.tabId === that._activeTabId)?.apiObject; + }, + get tabs() { + return that._tabs.map(tab => tab.apiObject); + } + }); + } + return this._apiObject; + } + + get groupId(): number { + return this._dto.groupId; + } + + findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { + return this._tabs.find(extHostTab => extHostTab.apiObject === apiTab); + } +} + +class ExtHostEditorTab { + private _apiObject: vscode.Tab | undefined; + private _dto: IEditorTabDto; + private _proxy: MainThreadEditorTabsShape; + + constructor(dto: IEditorTabDto, proxy: MainThreadEditorTabsShape) { + this._dto = dto; + this._proxy = proxy; + } + + get apiObject(): vscode.Tab { + // Don't want to lose reference to parent `this` in the getters + const that = this; + if (!this._apiObject) { + this._apiObject = Object.freeze({ + get isActive() { + return that._dto.isActive; + }, + get label() { + return that._dto.label; + }, + get resource() { + return URI.revive(that._dto.resource); + }, + get viewType() { + return that._dto.editorId; + }, + get isDirty() { + return that._dto.isDirty; + }, + get isPinned() { + return that._dto.isDirty; + }, + get viewColumn() { + return typeConverters.ViewColumn.to(that._dto.viewColumn); + }, + get kind() { + return that._dto.kind; + }, + get additionalResourcesAndViewTypes() { + return that._dto.additionalResourcesAndViewTypes.map(({ resource, viewId }) => ({ resource: URI.revive(resource), viewType: viewId })); + }, + move: async (index: number, viewColumn: ViewColumn) => { + this._proxy.$moveTab(that._dto, index, typeConverters.ViewColumn.from(viewColumn)); + return; + }, + close: async (preserveFocus) => { + this._proxy.$closeTab(that._dto, preserveFocus); + return; + } + }); + } + return this._apiObject; + } + + get tabId(): string { + return this._dto.id; + } + +} + export class ExtHostEditorTabs implements IExtHostEditorTabs { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadEditorTabsShape; private readonly _onDidChangeTabGroup = new Emitter(); - private readonly _onDidChangeActiveTabGroup = new Emitter(); + private readonly _onDidChangeActiveTabGroup = new Emitter(); - private _tabGroups: IEditorTabGroups = { + private _activeGroupId: number | undefined; + + private _tabGroups: vscode.TabGroups = { groups: [], activeTabGroup: undefined, onDidChangeTabGroup: this._onDidChangeTabGroup.event, onDidChangeActiveTabGroup: this._onDidChangeActiveTabGroup.event }; + private _extHostTabGroups: ExtHostEditorTabGroup[] = []; + constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs); } - get tabGroups(): IEditorTabGroups { + get tabGroups(): vscode.TabGroups { return this._tabGroups; } $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { // Clears the tab groups array this._tabGroups.groups.length = 0; - let activeGroupFound = false; - for (const group of tabGroups) { - let activeTab: IEditorTab | undefined; - const tabs = group.tabs.map(tab => { - const extHostTab = this.createExtHostTabObject(tab); - if (tab.isActive) { - activeTab = extHostTab; - } - return extHostTab; - }); - this._tabGroups.groups.push(Object.freeze({ - isActive: group.isActive, - viewColumn: typeConverters.ViewColumn.to(group.viewColumn), - activeTab, - tabs - })); - // If the currrent group is active, set the active group to be that group. - // We must use the same object so we pull from the array to allow for reference equality - if (group.isActive) { - activeGroupFound = true; - const oldActiveTabGroup = this._tabGroups.activeTabGroup; - this._tabGroups.activeTabGroup = this._tabGroups.groups[this._tabGroups.groups.length - 1]; - // Diff the old and current active group to decide if we should fire a change event - if (this.groupDiff(oldActiveTabGroup, this._tabGroups.activeTabGroup)) { - this._onDidChangeActiveTabGroup.fire(this._tabGroups.activeTabGroup); - } - } + this._extHostTabGroups = tabGroups.map(tabGroup => { + const group = new ExtHostEditorTabGroup(tabGroup, this._proxy); + return group; + }); + for (const group of this._extHostTabGroups) { + this._tabGroups.groups.push(group.apiObject); } - // No active group was found in the model (most common case is model was empty) fire undefined event - if (!activeGroupFound) { - this._tabGroups.activeTabGroup = undefined; - this._onDidChangeActiveTabGroup.fire(undefined); + // Set the active tab group id + const activeTabGroup = this._extHostTabGroups.find(group => group.apiObject.isActive === true); + const activeTabGroupId = activeTabGroup?.groupId; + if (activeTabGroupId !== this._activeGroupId) { + this._activeGroupId = activeTabGroupId; + this._onDidChangeActiveTabGroup.fire(activeTabGroup?.apiObject); + // TODO @lramos15 how do we set this without messing up readonly + this._tabGroups.activeTabGroup = activeTabGroup?.apiObject; } this._onDidChangeTabGroup.fire(); } - private createExtHostTabObject(tabDto: IEditorTabDto): IEditorTab { - return Object.freeze({ - label: tabDto.label, - viewColumn: typeConverters.ViewColumn.to(tabDto.viewColumn), - resource: URI.revive(tabDto.resource), - additionalResourcesAndViewTypes: tabDto.additionalResourcesAndViewTypes.map(({ resource, viewId }) => ({ resource: URI.revive(resource), viewType: viewId })), - viewType: tabDto.editorId, - isActive: tabDto.isActive, - kind: tabDto.kind, - isDirty: tabDto.isDirty, - isPinned: tabDto.isPinned, - move: async (index: number, viewColumn: ViewColumn) => { - this._proxy.$moveTab(tabDto, index, typeConverters.ViewColumn.from(viewColumn)); - // TODO: Need an on did change tab event at the group level - return; - }, - close: async (preserveFocus) => { - await this._proxy.$closeTab(tabDto, preserveFocus); - // TODO: Need an on did change tab event at the group level - return; - } - }); - } - /** * Compares two groups determining if they're the same or different * @param group1 The first group to compare * @param group2 The second group to compare * @returns True if different, false otherwise */ - private groupDiff(group1: IEditorTabGroup | undefined, group2: IEditorTabGroup | undefined): boolean { - if (group1 === group2) { - return false; - } - // They would be reference equal if both undefined so one is undefined and one isn't hence different - if (!group1 || !group2) { - return true; - } - if (group1.isActive !== group2.isActive - || group1.viewColumn !== group2.viewColumn - || group1.tabs.length !== group2.tabs.length - ) { - return true; - } - for (let i = 0; i < group1.tabs.length; i++) { - if (this.tabDiff(group1.tabs[i], group2.tabs[i])) { - return true; - } - } - return false; - } + // private groupDiff(group1: IEditorTabGroup | undefined, group2: IEditorTabGroup | undefined): boolean { + // if (group1 === group2) { + // return false; + // } + // // They would be reference equal if both undefined so one is undefined and one isn't hence different + // if (!group1 || !group2) { + // return true; + // } + // if (group1.isActive !== group2.isActive + // || group1.viewColumn !== group2.viewColumn + // || group1.tabs.length !== group2.tabs.length + // ) { + // return true; + // } + // for (let i = 0; i < group1.tabs.length; i++) { + // if (this.tabDiff(group1.tabs[i], group2.tabs[i])) { + // return true; + // } + // } + // return false; + // } /** * Compares two tabs determining if they're the same or different @@ -166,34 +208,34 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { * @param tab2 The second tab to compare * @returns True if different, false otherwise */ - private tabDiff(tab1: IEditorTab | undefined, tab2: IEditorTab | undefined): boolean { - if (tab1 === tab2) { - return false; - } - // They would be reference equal if both undefined so one is undefined and one isn't therefore they're different - if (!tab1 || !tab2) { - return true; - } - if (tab1.label !== tab2.label - || tab1.viewColumn !== tab2.viewColumn - || tab1.resource?.toString() !== tab2.resource?.toString() - || tab1.viewType !== tab2.viewType - || tab1.isActive !== tab2.isActive - || tab1.isPinned !== tab2.isPinned - || tab1.isDirty !== tab2.isDirty - || tab1.additionalResourcesAndViewTypes.length !== tab2.additionalResourcesAndViewTypes.length - ) { - return true; - } - for (let i = 0; i < tab1.additionalResourcesAndViewTypes.length; i++) { - const tab1Resource = tab1.additionalResourcesAndViewTypes[i].resource; - const tab2Resource = tab2.additionalResourcesAndViewTypes[i].resource; - const tab1viewType = tab1.additionalResourcesAndViewTypes[i].viewType; - const tab2viewType = tab2.additionalResourcesAndViewTypes[i].viewType; - if (tab1Resource?.toString() !== tab2Resource?.toString() || tab1viewType !== tab2viewType) { - return true; - } - } - return false; - } + // private tabDiff(tab1: IEditorTab | undefined, tab2: IEditorTab | undefined): boolean { + // if (tab1 === tab2) { + // return false; + // } + // // They would be reference equal if both undefined so one is undefined and one isn't therefore they're different + // if (!tab1 || !tab2) { + // return true; + // } + // if (tab1.label !== tab2.label + // || tab1.viewColumn !== tab2.viewColumn + // || tab1.resource?.toString() !== tab2.resource?.toString() + // || tab1.viewType !== tab2.viewType + // || tab1.isActive !== tab2.isActive + // || tab1.isPinned !== tab2.isPinned + // || tab1.isDirty !== tab2.isDirty + // || tab1.additionalResourcesAndViewTypes.length !== tab2.additionalResourcesAndViewTypes.length + // ) { + // return true; + // } + // for (let i = 0; i < tab1.additionalResourcesAndViewTypes.length; i++) { + // const tab1Resource = tab1.additionalResourcesAndViewTypes[i].resource; + // const tab2Resource = tab2.additionalResourcesAndViewTypes[i].resource; + // const tab1viewType = tab1.additionalResourcesAndViewTypes[i].viewType; + // const tab2viewType = tab2.additionalResourcesAndViewTypes[i].viewType; + // if (tab1Resource?.toString() !== tab2Resource?.toString() || tab1viewType !== tab2viewType) { + // return true; + // } + // } + // return false; + // } } diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 10f6cd9a38d..43068f0ea0b 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import assert = require('assert'); import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, MainThreadEditorTabsShape, TabKind } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostEditorTabs, IEditorTabGroup } from 'vs/workbench/api/common/extHostEditorTabs'; +import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; suite('ExtHostEditorTabs', function () { @@ -62,7 +63,7 @@ suite('ExtHostEditorTabs', function () { viewColumn: 0, groupId: 12, tabs: [tab], - activeTab: undefined! // TODO@lramos15 unused + activeTab: tab }]); assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); const [first] = extHostEditorTabs.tabGroups.groups; @@ -113,7 +114,7 @@ suite('ExtHostEditorTabs', function () { activeTab: undefined }]); assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); - const activeTabGroup: IEditorTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + const activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); assert.strictEqual(activeTabGroup.tabs.length, 0); assert.strictEqual(count, 1); @@ -153,7 +154,8 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, first); }); - test('onDidChangeActiveTabGroup fires properly', function () { + // TODO @lramos15 Change this test because now it only fires when id changes + test.skip('onDidChangeActiveTabGroup fires properly', function () { const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock() { // override/implement $moveTab or $closeTab @@ -161,7 +163,7 @@ suite('ExtHostEditorTabs', function () { ); let count = 0; - let activeTabGroupFromEvent: IEditorTabGroup | undefined = undefined; + let activeTabGroupFromEvent: vscode.TabGroup | undefined = undefined; extHostEditorTabs.tabGroups.onDidChangeActiveTabGroup((tabGroup) => { count++; activeTabGroupFromEvent = tabGroup; @@ -180,7 +182,7 @@ suite('ExtHostEditorTabs', function () { }]; extHostEditorTabs.$acceptEditorTabModel(tabModel); assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); - let activeTabGroup: IEditorTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + let activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; assert.strictEqual(count, 1); assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); // Firing again with same model shouldn't cause a change diff --git a/src/vscode-dts/vscode.proposed.tabs.d.ts b/src/vscode-dts/vscode.proposed.tabs.d.ts index a17be05a413..42918d88181 100644 --- a/src/vscode-dts/vscode.proposed.tabs.d.ts +++ b/src/vscode-dts/vscode.proposed.tabs.d.ts @@ -107,7 +107,7 @@ declare module 'vscode' { /** * The currently active group */ - readonly activeTabGroup: TabGroup | undefined; + activeTabGroup: TabGroup | undefined; /** * An {@link Event} which fires when a group changes.