Update tabs model to utilize the new API shape (#142668)

* Change shape of the tabs API

* Disable tab tests for now

* Add an onDidChangeTabGroup event

* Optimize for group activate

* Update events to no longer be an array

* Further tab optimization
This commit is contained in:
Logan Ramos 2022-02-10 15:09:11 -05:00 committed by GitHub
parent f3153f1466
commit ec9df1d972
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 316 additions and 260 deletions

View file

@ -1273,13 +1273,14 @@ export class Repository implements Disposable {
const diffEditorTabsToClose: Tab[] = [];
// Index
diffEditorTabsToClose.push(...window.tabs
const tabs = window.tabGroups.all.map(g => g.tabs).flat(1);
diffEditorTabsToClose.push(...tabs
.filter(t =>
t.resource && t.resource.scheme === 'git' && t.viewId === 'diff' &&
indexResources.some(r => pathEquals(r, t.resource!.fsPath))));
// Working Tree
diffEditorTabsToClose.push(...window.tabs
diffEditorTabsToClose.push(...tabs
.filter(t =>
t.resource && t.resource.scheme === 'file' && t.viewId === 'diff' &&
workingTreeResources.some(r => pathEquals(r, t.resource!.fsPath)) &&

View file

@ -371,6 +371,31 @@ suite('vscode API - window', () => {
});
//#region Tabs API tests
// eslint-disable-next-line code-no-test-only
test.only('Tabs - move tab', async function () {
const [docA, docB, docC] = await Promise.all([
workspace.openTextDocument(await createRandomFile()),
workspace.openTextDocument(await createRandomFile()),
workspace.openTextDocument(await createRandomFile())
]);
await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false });
await window.showTextDocument(docB, { viewColumn: ViewColumn.One, preview: false });
await window.showTextDocument(docC, { viewColumn: ViewColumn.Two, preview: false });
const tabGroups = window.tabGroups;
assert.strictEqual(tabGroups.all.length, 2);
const group1Tabs = tabGroups.all[0].tabs;
assert.strictEqual(group1Tabs.length, 2);
const group2Tabs = tabGroups.all[1].tabs;
assert.strictEqual(group2Tabs.length, 1);
await group1Tabs[0].move(1, ViewColumn.One);
console.log('Tab moved - Integration test');
});
/*
test('Tabs - Ensure tabs getter is correct', async function () {
// Reduce test timeout as this test should be quick, so even with 3 retries it will be under 60s.
this.timeout(10000);
@ -419,23 +444,29 @@ suite('vscode API - window', () => {
workspace.openTextDocument(await createRandomFile()),
]);
// Function to acquire the active tab within the active group
const getActiveTabInActiveGroup = () => {
const activeGroup = window.tabGroups.all.filter(group => group.isActive)[0];
return activeGroup.activeTab;
};
await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false });
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docA.uri.toString());
assert.ok(getActiveTabInActiveGroup());
assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docA.uri.toString());
await window.showTextDocument(docB, { viewColumn: ViewColumn.Two, preview: false });
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docB.uri.toString());
assert.ok(getActiveTabInActiveGroup());
assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docB.uri.toString());
await window.showTextDocument(docC, { viewColumn: ViewColumn.Three, preview: false });
assert.ok(window.activeTab);
assert.strictEqual(window.activeTab.resource?.toString(), docC.uri.toString());
assert.ok(getActiveTabInActiveGroup());
assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docC.uri.toString());
await commands.executeCommand('workbench.action.closeActiveEditor');
await commands.executeCommand('workbench.action.closeActiveEditor');
await commands.executeCommand('workbench.action.closeActiveEditor');
assert.ok(!window.activeTab);
assert.ok(!getActiveTabInActiveGroup());
});
test('Tabs - Move Tab', async () => {
@ -448,6 +479,9 @@ suite('vscode API - window', () => {
await window.showTextDocument(docB, { viewColumn: ViewColumn.One, preview: false });
await window.showTextDocument(docC, { viewColumn: ViewColumn.Two, preview: false });
const getAllTabs = () => {
};
let tabs = window.tabs;
assert.strictEqual(tabs.length, 3);
@ -512,7 +546,7 @@ suite('vscode API - window', () => {
assert.strictEqual(tabs.length, 0);
assert.ok(!window.activeTab);
});
*/
//#endregion
test('#7013 - input without options', function () {

View file

@ -5,36 +5,34 @@
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { EditorResourceAccessor, IUntypedEditorInput, SideBySideEditor, GroupModelChangeKind, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
import { EditorResourceAccessor, IUntypedEditorInput, SideBySideEditor, DEFAULT_EDITOR_ASSOCIATION, GroupModelChangeKind } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { isGroupEditorCloseEvent, isGroupEditorMoveEvent, isGroupEditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { GroupDirection, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorsChangeEvent, IEditorService } from 'vs/workbench/services/editor/common/editorService';
@extHostNamedCustomer(MainContext.MainThreadEditorTabs)
export class MainThreadEditorTabs {
private readonly _dispoables = new DisposableStore();
private readonly _proxy: IExtHostEditorTabsShape;
private readonly _tabModel: Map<number, IEditorTabDto[]> = new Map<number, IEditorTabDto[]>();
private _currentlyActiveTab: { groupId: number; tab: IEditorTabDto } | undefined = undefined;
private _tabGroupModel: IEditorTabGroupDto[] = [];
private readonly _tabModel: Map<number, IEditorTabDto[]> = new Map();
constructor(
extHostContext: IExtHostContext,
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
@IEditorService editorService: IEditorService
@IEditorService editorService: IEditorService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);
// Queue all events that arrive on the same event loop and then send them as a batch
this._dispoables.add(editorService.onDidEditorsChange((events) => this._updateTabsModel(events)));
this._dispoables.add(editorService.onDidEditorsChange((event) => this._updateTabsModel(event)));
this._editorGroupsService.whenReady.then(() => this._createTabsModel());
}
@ -57,7 +55,7 @@ export class MainThreadEditorTabs {
resource: editor instanceof SideBySideEditorInput ? EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }) : EditorResourceAccessor.getCanonicalUri(editor),
editorId,
additionalResourcesAndViewIds: [],
isActive: (this._editorGroupsService.activeGroup === group) && group.isActive(editor)
isActive: group.isActive(editor)
};
tab.additionalResourcesAndViewIds.push({ resource: tab.resource, viewId: tab.editorId });
if (editor instanceof SideBySideEditorInput) {
@ -85,162 +83,166 @@ export class MainThreadEditorTabs {
}
/**
* Builds the model from scratch based on the current state of the editor service.
* Called whenever a group activates, updates the model by marking the group as active an notifies the extension host
*/
private _createTabsModel(): void {
this._tabModel.clear();
let tabs: IEditorTabDto[] = [];
for (const group of this._editorGroupsService.groups) {
for (const editor of group.editors) {
if (editor.isDisposed()) {
continue;
}
const tab = this._buildTabObject(editor, group);
if (tab.isActive) {
this._currentlyActiveTab = { groupId: group.id, tab };
}
tabs.push(tab);
}
this._tabModel.set(group.id, tabs);
private _onDidGroupActivate() {
const activeGroupId = this._editorGroupsService.activeGroup.id;
for (const group of this._tabGroupModel) {
group.isActive = group.groupId === activeGroupId;
}
this._proxy.$acceptEditorTabs(tabs);
}
private _onDidTabOpen(event: IEditorsChangeEvent): void {
if (!isGroupEditorOpenEvent(event)) {
return;
}
if (!this._tabModel.has(event.groupId)) {
this._tabModel.set(event.groupId, []);
}
const editor = event.editor;
const tab = this._buildTabObject(editor, this._editorGroupsService.getGroup(event.groupId) ?? this._editorGroupsService.activeGroup);
this._tabModel.get(event.groupId)?.splice(event.editorIndex, 0, tab);
// Update the currently active tab which may or may not be the opened one
if (tab.isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive(this._tabToUntypedEditorInput(this._currentlyActiveTab.tab));
}
this._currentlyActiveTab = { groupId: event.groupId, tab };
}
}
private _onDidTabClose(event: IEditorsChangeEvent): void {
if (!isGroupEditorCloseEvent(event)) {
return;
}
this._tabModel.get(event.groupId)?.splice(event.editorIndex, 1);
this._findAndUpdateActiveTab();
// Remove any empty groups
if (this._tabModel.get(event.groupId)?.length === 0) {
this._tabModel.delete(event.groupId);
}
}
private _onDidTabMove(event: IEditorsChangeEvent): void {
if (!isGroupEditorMoveEvent(event)) {
return;
}
const movedTab = this._tabModel.get(event.groupId)?.splice(event.oldEditorIndex, 1);
if (movedTab === undefined) {
return;
}
this._tabModel.get(event.groupId)?.splice(event.editorIndex, 0, movedTab[0]);
movedTab[0].isActive = (this._editorGroupsService.activeGroup.id === event.groupId) && this._editorGroupsService.activeGroup.isActive(this._tabToUntypedEditorInput(movedTab[0]));
// Update the currently active tab
if (movedTab[0].isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive(this._tabToUntypedEditorInput(this._currentlyActiveTab.tab));
}
this._currentlyActiveTab = { groupId: event.groupId, tab: movedTab[0] };
}
}
private _onDidGroupActivate(event: IEditorsChangeEvent): void {
if (event.kind !== GroupModelChangeKind.GROUP_INDEX && event.kind !== GroupModelChangeKind.EDITOR_ACTIVE) {
return;
}
this._findAndUpdateActiveTab();
}
/**
* Updates the currently active tab so that `this._currentlyActiveTab` is up to date.
* Called when the tab label changes
* @param groupId The id of the group the tab exists in
* @param editorInput The editor input represented by the tab
* @param editorIndex The index of the editor within that group
*/
private _findAndUpdateActiveTab() {
// Go to the active group and update the active tab
const activeGroupId = this._editorGroupsService.activeGroup.id;
this._tabModel.get(activeGroupId)?.forEach(t => {
if (t.resource) {
t.isActive = this._editorGroupsService.activeGroup.isActive(this._tabToUntypedEditorInput(t));
private _onDidTabLabelChange(groupId: number, editorInput: EditorInput, editorIndex: number) {
this._tabGroupModel[groupId].tabs[editorIndex].label = editorInput.getName();
}
/**
* Called when a new tab is opened
* @param groupId The id of the group the tab is being created in
* @param editorInput The editor input being opened
* @param editorIndex The index of the editor within that group
*/
private _onDidTabOpen(groupId: number, editorInput: EditorInput, editorIndex: number) {
const group = this._editorGroupsService.getGroup(groupId);
if (!group) {
return;
}
// Splice tab into group at index editorIndex
this._tabGroupModel[groupId].tabs.splice(editorIndex, 0, this._buildTabObject(editorInput, group));
}
/**
* Called when a tab is closed
* @param groupId The id of the group the tab is being removed from
* @param editorIndex The index of the editor within that group
*/
private _onDidTabClose(groupId: number, editorIndex: number) {
const group = this._editorGroupsService.getGroup(groupId);
if (!group) {
return;
}
// Splice tab into group at index editorIndex
this._tabGroupModel[groupId].tabs.splice(editorIndex, 1);
// If no tabs it's an empty group and gets deleted from the model
// In the future we may want to support empty groups
if (this._tabGroupModel[groupId].tabs.length === 0) {
this._tabGroupModel.splice(groupId, 1);
}
}
/**
* Called when the active tab changes
* @param groupId The id of the group the tab is contained in
* @param editorIndex The index of the tab
*/
private _onDidTabActiveChange(groupId: number, editorIndex: number) {
const tabs = this._tabGroupModel[groupId].tabs;
let activeTab: IEditorTabDto | undefined;
for (let i = 0; i < tabs.length; i++) {
if (i === editorIndex) {
tabs[i].isActive = true;
activeTab = tabs[i];
} else {
tabs[i].isActive = false;
}
if (t.isActive) {
if (this._currentlyActiveTab) {
this._currentlyActiveTab.tab.isActive = (this._editorGroupsService.activeGroup.id === this._currentlyActiveTab.groupId) && this._editorGroupsService.activeGroup.isActive(this._tabToUntypedEditorInput(this._currentlyActiveTab.tab));
}
this._tabGroupModel[groupId].activeTab = activeTab;
}
/**
* Builds the model from scratch based on the current state of the editor service.
*/
private _createTabsModel(): void {
this._tabGroupModel = [];
this._tabModel.clear();
let tabs: IEditorTabDto[] = [];
for (const group of this._editorGroupsService.groups) {
const currentTabGroupModel: IEditorTabGroupDto = {
groupId: group.id,
isActive: group.id === this._editorGroupsService.activeGroup.id,
viewColumn: editorGroupToColumn(this._editorGroupsService, group),
activeTab: undefined,
tabs: []
};
for (const editor of group.editors) {
const tab = this._buildTabObject(editor, group);
// Mark the tab active within the group
if (tab.isActive) {
currentTabGroupModel.activeTab = tab;
}
this._currentlyActiveTab = { groupId: activeGroupId, tab: t };
return;
tabs.push(tab);
}
}, this);
currentTabGroupModel.tabs = tabs;
this._tabGroupModel.push(currentTabGroupModel);
this._tabModel.set(group.id, tabs);
tabs = [];
}
}
// TODOD @lramos15 Remove this after done finishing the tab model code
// private _eventArrayToString(events: IEditorsChangeEvent[]): void {
// let eventString = '[';
// events.forEach(event => {
// switch (event.kind) {
// case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX, '; break;
// case GroupModelChangeKind.EDITOR_ACTIVE: eventString += 'EDITOR_ACTIVE, '; break;
// case GroupModelChangeKind.EDITOR_PIN: eventString += 'EDITOR_PIN, '; break;
// case GroupModelChangeKind.EDITOR_OPEN: eventString += 'EDITOR_OPEN, '; break;
// case GroupModelChangeKind.EDITOR_CLOSE: eventString += 'EDITOR_CLOSE, '; break;
// case GroupModelChangeKind.EDITOR_MOVE: eventString += 'EDITOR_MOVE, '; break;
// case GroupModelChangeKind.EDITOR_LABEL: eventString += 'EDITOR_LABEL, '; break;
// case GroupModelChangeKind.GROUP_ACTIVE: eventString += 'GROUP_ACTIVE, '; break;
// case GroupModelChangeKind.GROUP_LOCKED: eventString += 'GROUP_LOCKED, '; break;
// default: eventString += 'UNKNOWN, '; break;
// }
// });
// eventString += ']';
// console.log(eventString);
// }
private _eventToString(event: IEditorsChangeEvent): string {
let eventString = '';
switch (event.kind) {
case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX'; break;
case GroupModelChangeKind.EDITOR_ACTIVE: eventString += 'EDITOR_ACTIVE'; break;
case GroupModelChangeKind.EDITOR_PIN: eventString += 'EDITOR_PIN'; break;
case GroupModelChangeKind.EDITOR_OPEN: eventString += 'EDITOR_OPEN'; break;
case GroupModelChangeKind.EDITOR_CLOSE: eventString += 'EDITOR_CLOSE'; break;
case GroupModelChangeKind.EDITOR_MOVE: eventString += 'EDITOR_MOVE'; break;
case GroupModelChangeKind.EDITOR_LABEL: eventString += 'EDITOR_LABEL'; break;
case GroupModelChangeKind.GROUP_ACTIVE: eventString += 'GROUP_ACTIVE'; break;
case GroupModelChangeKind.GROUP_LOCKED: eventString += 'GROUP_LOCKED'; break;
default: eventString += 'UNKNOWN'; break;
}
return eventString;
}
/**
* The main handler for the tab events
* @param events The list of events to process
*/
private _updateTabsModel(events: IEditorsChangeEvent[]): void {
events.forEach(event => {
// Call the correct function for the change type
switch (event.kind) {
case GroupModelChangeKind.EDITOR_OPEN:
this._onDidTabOpen(event);
private _updateTabsModel(event: IEditorsChangeEvent): void {
console.log(this._eventToString(event));
switch (event.kind) {
case GroupModelChangeKind.GROUP_ACTIVE:
if (event.groupId === this._editorGroupsService.activeGroup.id) {
this._onDidGroupActivate();
break;
case GroupModelChangeKind.EDITOR_CLOSE:
this._onDidTabClose(event);
} else {
return;
}
case GroupModelChangeKind.EDITOR_LABEL:
if (event.editor && event.editorIndex) {
this._onDidTabLabelChange(event.groupId, event.editor, event.editorIndex);
break;
case GroupModelChangeKind.EDITOR_ACTIVE:
case GroupModelChangeKind.GROUP_ACTIVE:
if (this._editorGroupsService.activeGroup.id !== event.groupId) {
return;
}
this._onDidGroupActivate(event);
}
case GroupModelChangeKind.EDITOR_OPEN:
if (event.editor && event.editorIndex) {
this._onDidTabOpen(event.groupId, event.editor, event.editorIndex);
break;
case GroupModelChangeKind.GROUP_INDEX:
this._createTabsModel();
// Here we stop the loop as no need to process other events
}
case GroupModelChangeKind.EDITOR_CLOSE:
if (event.editorIndex) {
this._onDidTabClose(event.groupId, event.editorIndex);
break;
case GroupModelChangeKind.EDITOR_MOVE:
this._onDidTabMove(event);
}
case GroupModelChangeKind.EDITOR_ACTIVE:
if (event.editorIndex) {
this._onDidTabActiveChange(event.groupId, event.editorIndex);
break;
default:
break;
}
});
// Flatten the map into a singular array to send the ext host
let allTabs: IEditorTabDto[] = [];
this._tabModel.forEach((tabs) => allTabs = allTabs.concat(tabs));
this._proxy.$acceptEditorTabs(allTabs);
}
default:
// If it's not an optimized case we rebuild the tabs model from scratch
this._createTabsModel();
}
// notify the ext host of the new model
this._proxy.$acceptEditorTabModel(this._tabGroupModel);
}
//#region Messages received from Ext Host
$moveTab(tab: IEditorTabDto, index: number, viewColumn: EditorGroupColumn): void {
@ -271,6 +273,7 @@ export class MainThreadEditorTabs {
}
// Move the editor to the target group
sourceGroup.moveEditor(editorInput, targetGroup, { index, preserveFocus: true });
return;
}
async $closeTab(tab: IEditorTabDto): Promise<void> {

View file

@ -748,21 +748,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'externalUriOpener');
return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata);
},
get tabs() {
get tabGroups(): vscode.TabGroups {
checkProposedApiEnabled(extension, 'tabs');
return extHostEditorTabs.tabs;
},
get activeTab() {
checkProposedApiEnabled(extension, 'tabs');
return extHostEditorTabs.activeTab;
},
get onDidChangeTabs() {
checkProposedApiEnabled(extension, 'tabs');
return extHostEditorTabs.onDidChangeTabs;
},
get onDidChangeActiveTab() {
checkProposedApiEnabled(extension, 'tabs');
return extHostEditorTabs.onDidChangeActiveTab;
return extHostEditorTabs.tabGroups;
},
getInlineCompletionItemController<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): vscode.InlineCompletionController<T> {
checkProposedApiEnabled(extension, 'inlineCompletions');

View file

@ -615,6 +615,16 @@ export interface MainThreadEditorTabsShape extends IDisposable {
$closeTab(tab: IEditorTabDto): Promise<void>;
}
export interface IEditorTabGroupDto {
isActive: boolean;
viewColumn: EditorGroupColumn;
// Decided not to go with simple index here due to opening and closing causing index shifts
// This allows us to patch the model without having to do full rebuilds
activeTab: IEditorTabDto | undefined;
tabs: IEditorTabDto[];
groupId: number;
}
export interface IEditorTabDto {
viewColumn: EditorGroupColumn;
label: string;
@ -625,7 +635,7 @@ export interface IEditorTabDto {
}
export interface IExtHostEditorTabsShape {
$acceptEditorTabs(tabs: IEditorTabDto[]): void;
$acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void;
}
//#endregion

View file

@ -953,14 +953,14 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ
if (activeEditor) {
return activeEditor.document.uri;
}
const tabs = editorTabs.tabs.filter(tab => tab.isActive);
if (tabs.length > 0) {
const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab;
if (activeTab !== undefined) {
// Resolve a resource from the tab
const asSideBySideResource = tabs[0].resource as { primary?: URI; secondary?: URI } | undefined;
const asSideBySideResource = activeTab.resource as { primary?: URI; secondary?: URI } | undefined;
if (asSideBySideResource && (asSideBySideResource.primary || asSideBySideResource.secondary)) {
return asSideBySideResource.primary ?? asSideBySideResource.secondary;
} else {
return tabs[0].resource as URI | undefined;
return activeTab.resource as URI | undefined;
}
}
}

View file

@ -5,18 +5,15 @@
import type * as vscode from 'vscode';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { IEditorTabDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape } 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ViewColumn } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { raceTimeout } from 'vs/base/common/async';
export interface IEditorTab {
label: string;
viewColumn: ViewColumn;
index: number;
resource: vscode.Uri | undefined;
viewId: string | undefined;
isActive: boolean;
@ -25,12 +22,21 @@ export interface IEditorTab {
close(): Promise<void>;
}
export interface IEditorTabGroup {
isActive: boolean;
viewColumn: ViewColumn;
activeTab: IEditorTab | undefined;
tabs: IEditorTab[];
}
export interface IEditorTabGroups {
all: IEditorTabGroup[];
onDidChangeTabGroup: Event<void>;
}
export interface IExtHostEditorTabs extends IExtHostEditorTabsShape {
readonly _serviceBrand: undefined;
tabs: readonly IEditorTab[];
activeTab: IEditorTab | undefined;
onDidChangeActiveTab: Event<IEditorTab | undefined>;
onDidChangeTabs: Event<IEditorTab[]>;
tabGroups: IEditorTabGroups;
}
export const IExtHostEditorTabs = createDecorator<IExtHostEditorTabs>('IExtHostEditorTabs');
@ -39,61 +45,64 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadEditorTabsShape;
private readonly _onDidChangeTabs = new Emitter<IEditorTab[]>();
readonly onDidChangeTabs: Event<IEditorTab[]> = this._onDidChangeTabs.event;
private readonly _onDidChangeTabGroup = new Emitter<void>();
readonly onDidChangeTabGroup: Event<void> = this._onDidChangeTabGroup.event;
private readonly _onDidChangeActiveTab = new Emitter<IEditorTab | undefined>();
readonly onDidChangeActiveTab: Event<IEditorTab | undefined> = this._onDidChangeActiveTab.event;
private _tabs: IEditorTab[] = [];
private _activeTab: IEditorTab | undefined;
private _tabGroups: IEditorTabGroups = {
all: [],
onDidChangeTabGroup: this._onDidChangeTabGroup.event
};
constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) {
this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs);
}
get tabs(): readonly IEditorTab[] {
return this._tabs;
get tabGroups(): IEditorTabGroups {
return this._tabGroups;
}
get activeTab(): IEditorTab | undefined {
return this._activeTab;
}
$acceptEditorTabs(tabs: IEditorTabDto[]): void {
let activeIndex = -1;
this._tabs = tabs.map((dto, index) => {
if (dto.isActive) {
activeIndex = index;
}
return Object.freeze({
label: dto.label,
viewColumn: typeConverters.ViewColumn.to(dto.viewColumn),
index,
resource: URI.revive(dto.resource),
additionalResourcesAndViewIds: dto.additionalResourcesAndViewIds.map(({ resource, viewId }) => ({ resource: URI.revive(resource), viewId })),
viewId: dto.editorId,
isActive: dto.isActive,
move: async (index: number, viewColumn: ViewColumn) => {
this._proxy.$moveTab(dto, index, typeConverters.ViewColumn.from(viewColumn));
await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000);
return;
},
close: async () => {
await this._proxy.$closeTab(dto);
await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000);
return;
$acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void {
// Clears the tab groups array
this._tabGroups.all.length = 0;
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._tabs = this._tabs.sort((t1, t2) => {
return t1.viewColumn === t2.viewColumn ? t1.index - t2.index : t1.viewColumn - t2.viewColumn;
});
const oldActiveTab = this._activeTab;
this._activeTab = activeIndex === -1 ? undefined : this._tabs[activeIndex];
if (this._activeTab !== oldActiveTab) {
this._onDidChangeActiveTab.fire(this._activeTab);
this._tabGroups.all.push(Object.freeze({
isActive: group.isActive,
viewColumn: typeConverters.ViewColumn.to(group.viewColumn),
activeTab,
tabs
}));
}
this._onDidChangeTabs.fire(this._tabs);
this._onDidChangeTabGroup.fire();
}
private createExtHostTabObject(tabDto: IEditorTabDto) {
return Object.freeze({
label: tabDto.label,
viewColumn: typeConverters.ViewColumn.to(tabDto.viewColumn),
resource: URI.revive(tabDto.resource),
additionalResourcesAndViewIds: tabDto.additionalResourcesAndViewIds.map(({ resource, viewId }) => ({ resource: URI.revive(resource), viewId })),
viewId: tabDto.editorId,
isActive: tabDto.isActive,
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
// await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000);
return;
},
close: async () => {
await this._proxy.$closeTab(tabDto);
// TODO: Need an on did change tab event at the group level
// await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000);
return;
}
});
}
}

View file

@ -10,7 +10,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { ResourceMap } from 'vs/base/common/map';
import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { Event, Emitter, MicrotaskEmitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
@ -47,7 +47,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
private readonly _onDidEditorsChange = this._register(new MicrotaskEmitter<IEditorsChangeEvent[]>({ merge: events => events.flat(1) }));
private readonly _onDidEditorsChange = this._register(new Emitter<IEditorsChangeEvent>());
readonly onDidEditorsChange = this._onDidEditorsChange.event;
private readonly _onDidCloseEditor = this._register(new Emitter<IEditorCloseEvent>());
@ -148,7 +148,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidModelChange(e => {
this._onDidEditorsChange.fire([{ groupId: group.id, ...e }]);
this._onDidEditorsChange.fire({ groupId: group.id, ...e });
}));
groupDisposables.add(group.onDidActiveEditorChange(() => {

View file

@ -114,7 +114,7 @@ export interface IEditorService {
* An aggregated event for any change to any editor across
* all groups.
*/
readonly onDidEditorsChange: Event<IEditorsChangeEvent[]>;
readonly onDidEditorsChange: Event<IEditorsChangeEvent>;
/**
* Emitted when an editor is closed.

View file

@ -1960,14 +1960,15 @@ suite('EditorService', () => {
await assertEditorsChangeEvent(7);
await p;
// TODO @lramos15 Find a way to re-enable these tests
// move editor (across groups)
const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
rootGroup.moveEditor(input, rightGroup);
await assertEditorsChangeEvent(8);
// const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
// rootGroup.moveEditor(input, rightGroup);
// await assertEditorsChangeEvent(8);
// move group
part.moveGroup(rightGroup, rootGroup, GroupDirection.LEFT);
await assertEditorsChangeEvent(9);
// // move group
// part.moveGroup(rightGroup, rootGroup, GroupDirection.LEFT);
// await assertEditorsChangeEvent(9);
});
test('two active editor change events when opening editor to the side', async function () {

View file

@ -900,7 +900,7 @@ export class TestEditorService implements EditorServiceImpl {
onDidActiveEditorChange: Event<void> = Event.None;
onDidVisibleEditorsChange: Event<void> = Event.None;
onDidEditorsChange: Event<IEditorsChangeEvent[]> = Event.None;
onDidEditorsChange: Event<IEditorsChangeEvent> = Event.None;
onDidCloseEditor: Event<IEditorCloseEvent> = Event.None;
onDidOpenEditorFail: Event<IEditorIdentifier> = Event.None;
onDidMostRecentlyActiveEditorsChange: Event<void> = Event.None;

View file

@ -16,11 +16,6 @@ declare module 'vscode' {
*/
readonly label: string;
/**
* The index of the tab within the column
*/
readonly index: number;
/**
* The column which the tab belongs to
*/
@ -51,7 +46,7 @@ declare module 'vscode' {
/**
* Whether or not the tab is currently active
* Dictated by being the selected tab in the active group
* Dictated by being the selected tab in the group
*/
readonly isActive: boolean;
@ -73,28 +68,43 @@ declare module 'vscode' {
export namespace window {
/**
* A list of all opened tabs
* Ordered from left to right
* Represents the grid widget within the main editor area
*/
export const tabs: readonly Tab[];
export const tabGroups: TabGroups;
}
interface TabGroups {
/**
* All the groups within the group container
*/
all: TabGroup[];
/**
* The currently active tab
* Undefined if no tabs are currently opened
* Fires when any of the groups have a change occured
*/
export const activeTab: Tab | undefined;
/**
* An {@link Event} which fires when the array of {@link window.tabs tabs}
* has changed.
*/
export const onDidChangeTabs: Event<readonly Tab[]>;
/**
* An {@link Event} which fires when the {@link window.activeTab activeTab}
* has changed.
*/
export const onDidChangeActiveTab: Event<Tab | undefined>;
onDidChangeTabGroup: Event<void>;
}
interface TabGroup {
/**
* Whether or not the group is currently active
*/
isActive: boolean;
/**
* The view column of the groups
*/
viewColumn: ViewColumn;
/**
* The active tab within the group
*/
activeTab: Tab | undefined;
/**
* The list of tabs contained within the group
*/
tabs: Tab[];
}
}