Path: blob/main/src/vs/workbench/api/browser/mainThreadEditorTabs.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event } from '../../../base/common/event.js';6import { DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';7import { isEqual } from '../../../base/common/resources.js';8import { URI } from '../../../base/common/uri.js';9import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';10import { ILogService } from '../../../platform/log/common/log.js';11import { AnyInputDto, ExtHostContext, IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextDiffInputDto } from '../common/extHost.protocol.js';12import { EditorResourceAccessor, GroupModelChangeKind, SideBySideEditor } from '../../common/editor.js';13import { DiffEditorInput } from '../../common/editor/diffEditorInput.js';14import { isGroupEditorMoveEvent } from '../../common/editor/editorGroupModel.js';15import { EditorInput } from '../../common/editor/editorInput.js';16import { SideBySideEditorInput } from '../../common/editor/sideBySideEditorInput.js';17import { AbstractTextResourceEditorInput } from '../../common/editor/textResourceEditorInput.js';18import { ChatEditorInput } from '../../contrib/chat/browser/chatEditorInput.js';19import { CustomEditorInput } from '../../contrib/customEditor/browser/customEditorInput.js';20import { InteractiveEditorInput } from '../../contrib/interactive/browser/interactiveEditorInput.js';21import { MergeEditorInput } from '../../contrib/mergeEditor/browser/mergeEditorInput.js';22import { MultiDiffEditorInput } from '../../contrib/multiDiffEditor/browser/multiDiffEditorInput.js';23import { NotebookEditorInput } from '../../contrib/notebook/common/notebookEditorInput.js';24import { TerminalEditorInput } from '../../contrib/terminal/browser/terminalEditorInput.js';25import { WebviewInput } from '../../contrib/webviewPanel/browser/webviewEditorInput.js';26import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from '../../services/editor/common/editorGroupColumn.js';27import { GroupDirection, IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from '../../services/editor/common/editorGroupsService.js';28import { IEditorsChangeEvent, IEditorService, SIDE_GROUP } from '../../services/editor/common/editorService.js';29import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';3031interface TabInfo {32tab: IEditorTabDto;33group: IEditorGroup;34editorInput: EditorInput;35}36@extHostNamedCustomer(MainContext.MainThreadEditorTabs)37export class MainThreadEditorTabs implements MainThreadEditorTabsShape {3839private readonly _dispoables = new DisposableStore();40private readonly _proxy: IExtHostEditorTabsShape;41// List of all groups and their corresponding tabs, this is **the** model42private _tabGroupModel: IEditorTabGroupDto[] = [];43// Lookup table for finding group by id44private readonly _groupLookup: Map<number, IEditorTabGroupDto> = new Map();45// Lookup table for finding tab by id46private readonly _tabInfoLookup: Map<string, TabInfo> = new Map();47// Tracks the currently open MultiDiffEditorInputs to listen to resource changes48private readonly _multiDiffEditorInputListeners: DisposableMap<MultiDiffEditorInput> = new DisposableMap();4950constructor(51extHostContext: IExtHostContext,52@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,53@IConfigurationService private readonly _configurationService: IConfigurationService,54@ILogService private readonly _logService: ILogService,55@IEditorService editorService: IEditorService56) {5758this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);5960// Main listener which responds to events from the editor service61this._dispoables.add(editorService.onDidEditorsChange((event) => {62try {63this._updateTabsModel(event);64} catch {65this._logService.error('Failed to update model, rebuilding');66this._createTabsModel();67}68}));6970this._dispoables.add(this._multiDiffEditorInputListeners);7172// Structural group changes (add, remove, move, etc) are difficult to patch.73// Since they happen infrequently we just rebuild the entire model74this._dispoables.add(this._editorGroupsService.onDidAddGroup(() => this._createTabsModel()));75this._dispoables.add(this._editorGroupsService.onDidRemoveGroup(() => this._createTabsModel()));7677// Once everything is read go ahead and initialize the model78this._editorGroupsService.whenReady.then(() => this._createTabsModel());79}8081dispose(): void {82this._groupLookup.clear();83this._tabInfoLookup.clear();84this._dispoables.dispose();85}8687/**88* Creates a tab object with the correct properties89* @param editor The editor input represented by the tab90* @param group The group the tab is in91* @returns A tab object92*/93private _buildTabObject(group: IEditorGroup, editor: EditorInput, editorIndex: number): IEditorTabDto {94const editorId = editor.editorId;95const tab: IEditorTabDto = {96id: this._generateTabId(editor, group.id),97label: editor.getName(),98editorId,99input: this._editorInputToDto(editor),100isPinned: group.isSticky(editorIndex),101isPreview: !group.isPinned(editorIndex),102isActive: group.isActive(editor),103isDirty: editor.isDirty()104};105return tab;106}107108private _editorInputToDto(editor: EditorInput): AnyInputDto {109110if (editor instanceof MergeEditorInput) {111return {112kind: TabInputKind.TextMergeInput,113base: editor.base,114input1: editor.input1.uri,115input2: editor.input2.uri,116result: editor.resource117};118}119120if (editor instanceof AbstractTextResourceEditorInput) {121return {122kind: TabInputKind.TextInput,123uri: editor.resource124};125}126127if (editor instanceof SideBySideEditorInput && !(editor instanceof DiffEditorInput)) {128const primaryResource = editor.primary.resource;129const secondaryResource = editor.secondary.resource;130// If side by side editor with same resource on both sides treat it as a singular tab kind131if (editor.primary instanceof AbstractTextResourceEditorInput132&& editor.secondary instanceof AbstractTextResourceEditorInput133&& isEqual(primaryResource, secondaryResource)134&& primaryResource135&& secondaryResource136) {137return {138kind: TabInputKind.TextInput,139uri: primaryResource140};141}142return { kind: TabInputKind.UnknownInput };143}144145if (editor instanceof NotebookEditorInput) {146return {147kind: TabInputKind.NotebookInput,148notebookType: editor.viewType,149uri: editor.resource150};151}152153if (editor instanceof CustomEditorInput) {154return {155kind: TabInputKind.CustomEditorInput,156viewType: editor.viewType,157uri: editor.resource,158};159}160161if (editor instanceof WebviewInput) {162return {163kind: TabInputKind.WebviewEditorInput,164viewType: editor.viewType165};166}167168if (editor instanceof TerminalEditorInput) {169return {170kind: TabInputKind.TerminalEditorInput171};172}173174if (editor instanceof DiffEditorInput) {175if (editor.modified instanceof AbstractTextResourceEditorInput && editor.original instanceof AbstractTextResourceEditorInput) {176return {177kind: TabInputKind.TextDiffInput,178modified: editor.modified.resource,179original: editor.original.resource180};181}182if (editor.modified instanceof NotebookEditorInput && editor.original instanceof NotebookEditorInput) {183return {184kind: TabInputKind.NotebookDiffInput,185notebookType: editor.original.viewType,186modified: editor.modified.resource,187original: editor.original.resource188};189}190}191192if (editor instanceof InteractiveEditorInput) {193return {194kind: TabInputKind.InteractiveEditorInput,195uri: editor.resource,196inputBoxUri: editor.inputResource197};198}199200if (editor instanceof ChatEditorInput) {201return {202kind: TabInputKind.ChatEditorInput,203};204}205206if (editor instanceof MultiDiffEditorInput) {207const diffEditors: TextDiffInputDto[] = [];208for (const resource of (editor?.resources.get() ?? [])) {209if (resource.originalUri && resource.modifiedUri) {210diffEditors.push({211kind: TabInputKind.TextDiffInput,212original: resource.originalUri,213modified: resource.modifiedUri214});215}216}217218return {219kind: TabInputKind.MultiDiffEditorInput,220diffEditors221};222}223224return { kind: TabInputKind.UnknownInput };225}226227/**228* Generates a unique id for a tab229* @param editor The editor input230* @param groupId The group id231* @returns A unique identifier for a specific tab232*/233private _generateTabId(editor: EditorInput, groupId: number) {234let resourceString: string | undefined;235// Properly get the resource and account for side by side editors236const resource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.BOTH });237if (resource instanceof URI) {238resourceString = resource.toString();239} else {240resourceString = `${resource?.primary?.toString()}-${resource?.secondary?.toString()}`;241}242return `${groupId}~${editor.editorId}-${editor.typeId}-${resourceString} `;243}244245/**246* Called whenever a group activates, updates the model by marking the group as active an notifies the extension host247*/248private _onDidGroupActivate() {249const activeGroupId = this._editorGroupsService.activeGroup.id;250const activeGroup = this._groupLookup.get(activeGroupId);251if (activeGroup) {252// Ok not to loop as exthost accepts last active group253activeGroup.isActive = true;254this._proxy.$acceptTabGroupUpdate(activeGroup);255}256}257258/**259* Called when the tab label changes260* @param groupId The id of the group the tab exists in261* @param editorInput The editor input represented by the tab262*/263private _onDidTabLabelChange(groupId: number, editorInput: EditorInput, editorIndex: number) {264const tabId = this._generateTabId(editorInput, groupId);265const tabInfo = this._tabInfoLookup.get(tabId);266// If tab is found patch, else rebuild267if (tabInfo) {268tabInfo.tab.label = editorInput.getName();269this._proxy.$acceptTabOperation({270groupId,271index: editorIndex,272tabDto: tabInfo.tab,273kind: TabModelOperationKind.TAB_UPDATE274});275} else {276this._logService.error('Invalid model for label change, rebuilding');277this._createTabsModel();278}279}280281/**282* Called when a new tab is opened283* @param groupId The id of the group the tab is being created in284* @param editorInput The editor input being opened285* @param editorIndex The index of the editor within that group286*/287private _onDidTabOpen(groupId: number, editorInput: EditorInput, editorIndex: number) {288const group = this._editorGroupsService.getGroup(groupId);289// Even if the editor service knows about the group the group might not exist yet in our model290const groupInModel = this._groupLookup.get(groupId) !== undefined;291// Means a new group was likely created so we rebuild the model292if (!group || !groupInModel) {293this._createTabsModel();294return;295}296const tabs = this._groupLookup.get(groupId)?.tabs;297if (!tabs) {298return;299}300// Splice tab into group at index editorIndex301const tabObject = this._buildTabObject(group, editorInput, editorIndex);302tabs.splice(editorIndex, 0, tabObject);303// Update lookup304const tabId = this._generateTabId(editorInput, groupId);305this._tabInfoLookup.set(tabId, { group, editorInput, tab: tabObject });306307if (editorInput instanceof MultiDiffEditorInput) {308this._multiDiffEditorInputListeners.set(editorInput, Event.fromObservableLight(editorInput.resources)(() => {309const tabInfo = this._tabInfoLookup.get(tabId);310if (!tabInfo) {311return;312}313tabInfo.tab = this._buildTabObject(group, editorInput, editorIndex);314this._proxy.$acceptTabOperation({315groupId,316index: editorIndex,317tabDto: tabInfo.tab,318kind: TabModelOperationKind.TAB_UPDATE319});320}));321}322323this._proxy.$acceptTabOperation({324groupId,325index: editorIndex,326tabDto: tabObject,327kind: TabModelOperationKind.TAB_OPEN328});329}330331/**332* Called when a tab is closed333* @param groupId The id of the group the tab is being removed from334* @param editorIndex The index of the editor within that group335*/336private _onDidTabClose(groupId: number, editorIndex: number) {337const group = this._editorGroupsService.getGroup(groupId);338const tabs = this._groupLookup.get(groupId)?.tabs;339// Something is wrong with the model state so we rebuild340if (!group || !tabs) {341this._createTabsModel();342return;343}344// Splice tab into group at index editorIndex345const removedTab = tabs.splice(editorIndex, 1);346347// Index must no longer be valid so we return prematurely348if (removedTab.length === 0) {349return;350}351352// Update lookup353this._tabInfoLookup.delete(removedTab[0]?.id ?? '');354355if (removedTab[0]?.input instanceof MultiDiffEditorInput) {356this._multiDiffEditorInputListeners.deleteAndDispose(removedTab[0]?.input);357}358359this._proxy.$acceptTabOperation({360groupId,361index: editorIndex,362tabDto: removedTab[0],363kind: TabModelOperationKind.TAB_CLOSE364});365}366367/**368* Called when the active tab changes369* @param groupId The id of the group the tab is contained in370* @param editorIndex The index of the tab371*/372private _onDidTabActiveChange(groupId: number, editorIndex: number) {373// TODO @lramos15 use the tab lookup here if possible. Do we have an editor input?!374const tabs = this._groupLookup.get(groupId)?.tabs;375if (!tabs) {376return;377}378const activeTab = tabs[editorIndex];379// No need to loop over as the exthost uses the most recently marked active tab380activeTab.isActive = true;381// Send DTO update to the exthost382this._proxy.$acceptTabOperation({383groupId,384index: editorIndex,385tabDto: activeTab,386kind: TabModelOperationKind.TAB_UPDATE387});388389}390391/**392* Called when the dirty indicator on the tab changes393* @param groupId The id of the group the tab is in394* @param editorIndex The index of the tab395* @param editor The editor input represented by the tab396*/397private _onDidTabDirty(groupId: number, editorIndex: number, editor: EditorInput) {398const tabId = this._generateTabId(editor, groupId);399const tabInfo = this._tabInfoLookup.get(tabId);400// Something wrong with the model state so we rebuild401if (!tabInfo) {402this._logService.error('Invalid model for dirty change, rebuilding');403this._createTabsModel();404return;405}406tabInfo.tab.isDirty = editor.isDirty();407this._proxy.$acceptTabOperation({408groupId,409index: editorIndex,410tabDto: tabInfo.tab,411kind: TabModelOperationKind.TAB_UPDATE412});413}414415/**416* Called when the tab is pinned/unpinned417* @param groupId The id of the group the tab is in418* @param editorIndex The index of the tab419* @param editor The editor input represented by the tab420*/421private _onDidTabPinChange(groupId: number, editorIndex: number, editor: EditorInput) {422const tabId = this._generateTabId(editor, groupId);423const tabInfo = this._tabInfoLookup.get(tabId);424const group = tabInfo?.group;425const tab = tabInfo?.tab;426// Something wrong with the model state so we rebuild427if (!group || !tab) {428this._logService.error('Invalid model for sticky change, rebuilding');429this._createTabsModel();430return;431}432// Whether or not the tab has the pin icon (internally it's called sticky)433tab.isPinned = group.isSticky(editorIndex);434this._proxy.$acceptTabOperation({435groupId,436index: editorIndex,437tabDto: tab,438kind: TabModelOperationKind.TAB_UPDATE439});440}441442/**443* Called when the tab is preview / unpreviewed444* @param groupId The id of the group the tab is in445* @param editorIndex The index of the tab446* @param editor The editor input represented by the tab447*/448private _onDidTabPreviewChange(groupId: number, editorIndex: number, editor: EditorInput) {449const tabId = this._generateTabId(editor, groupId);450const tabInfo = this._tabInfoLookup.get(tabId);451const group = tabInfo?.group;452const tab = tabInfo?.tab;453// Something wrong with the model state so we rebuild454if (!group || !tab) {455this._logService.error('Invalid model for sticky change, rebuilding');456this._createTabsModel();457return;458}459// Whether or not the tab has the pin icon (internally it's called pinned)460tab.isPreview = !group.isPinned(editorIndex);461this._proxy.$acceptTabOperation({462kind: TabModelOperationKind.TAB_UPDATE,463groupId,464tabDto: tab,465index: editorIndex466});467}468469private _onDidTabMove(groupId: number, editorIndex: number, oldEditorIndex: number, editor: EditorInput) {470const tabs = this._groupLookup.get(groupId)?.tabs;471// Something wrong with the model state so we rebuild472if (!tabs) {473this._logService.error('Invalid model for move change, rebuilding');474this._createTabsModel();475return;476}477478// Move tab from old index to new index479const removedTab = tabs.splice(oldEditorIndex, 1);480if (removedTab.length === 0) {481return;482}483tabs.splice(editorIndex, 0, removedTab[0]);484485// Notify exthost of move486this._proxy.$acceptTabOperation({487kind: TabModelOperationKind.TAB_MOVE,488groupId,489tabDto: removedTab[0],490index: editorIndex,491oldIndex: oldEditorIndex492});493}494495/**496* Builds the model from scratch based on the current state of the editor service.497*/498private _createTabsModel(): void {499if (this._editorGroupsService.groups.length === 0) {500return; // skip this invalid state, it may happen when the entire editor area is transitioning to other state ("editor working sets")501}502503this._tabGroupModel = [];504this._groupLookup.clear();505this._tabInfoLookup.clear();506let tabs: IEditorTabDto[] = [];507for (const group of this._editorGroupsService.groups) {508const currentTabGroupModel: IEditorTabGroupDto = {509groupId: group.id,510isActive: group.id === this._editorGroupsService.activeGroup.id,511viewColumn: editorGroupToColumn(this._editorGroupsService, group),512tabs: []513};514group.editors.forEach((editor, editorIndex) => {515const tab = this._buildTabObject(group, editor, editorIndex);516tabs.push(tab);517// Add information about the tab to the lookup518this._tabInfoLookup.set(this._generateTabId(editor, group.id), {519group,520tab,521editorInput: editor522});523});524currentTabGroupModel.tabs = tabs;525this._tabGroupModel.push(currentTabGroupModel);526this._groupLookup.set(group.id, currentTabGroupModel);527tabs = [];528}529// notify the ext host of the new model530this._proxy.$acceptEditorTabModel(this._tabGroupModel);531}532533// TODOD @lramos15 Remove this after done finishing the tab model code534// private _eventToString(event: IEditorsChangeEvent | IEditorsMoveEvent): string {535// let eventString = '';536// switch (event.kind) {537// case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX'; break;538// case GroupModelChangeKind.EDITOR_ACTIVE: eventString += 'EDITOR_ACTIVE'; break;539// case GroupModelChangeKind.EDITOR_PIN: eventString += 'EDITOR_PIN'; break;540// case GroupModelChangeKind.EDITOR_OPEN: eventString += 'EDITOR_OPEN'; break;541// case GroupModelChangeKind.EDITOR_CLOSE: eventString += 'EDITOR_CLOSE'; break;542// case GroupModelChangeKind.EDITOR_MOVE: eventString += 'EDITOR_MOVE'; break;543// case GroupModelChangeKind.EDITOR_LABEL: eventString += 'EDITOR_LABEL'; break;544// case GroupModelChangeKind.GROUP_ACTIVE: eventString += 'GROUP_ACTIVE'; break;545// case GroupModelChangeKind.GROUP_LOCKED: eventString += 'GROUP_LOCKED'; break;546// case GroupModelChangeKind.EDITOR_DIRTY: eventString += 'EDITOR_DIRTY'; break;547// case GroupModelChangeKind.EDITOR_STICKY: eventString += 'EDITOR_STICKY'; break;548// default: eventString += `UNKNOWN: ${event.kind}`; break;549// }550// return eventString;551// }552553/**554* The main handler for the tab events555* @param events The list of events to process556*/557private _updateTabsModel(changeEvent: IEditorsChangeEvent): void {558const event = changeEvent.event;559const groupId = changeEvent.groupId;560switch (event.kind) {561case GroupModelChangeKind.GROUP_ACTIVE:562if (groupId === this._editorGroupsService.activeGroup.id) {563this._onDidGroupActivate();564break;565} else {566return;567}568case GroupModelChangeKind.EDITOR_LABEL:569if (event.editor !== undefined && event.editorIndex !== undefined) {570this._onDidTabLabelChange(groupId, event.editor, event.editorIndex);571break;572}573case GroupModelChangeKind.EDITOR_OPEN:574if (event.editor !== undefined && event.editorIndex !== undefined) {575this._onDidTabOpen(groupId, event.editor, event.editorIndex);576break;577}578case GroupModelChangeKind.EDITOR_CLOSE:579if (event.editorIndex !== undefined) {580this._onDidTabClose(groupId, event.editorIndex);581break;582}583case GroupModelChangeKind.EDITOR_ACTIVE:584if (event.editorIndex !== undefined) {585this._onDidTabActiveChange(groupId, event.editorIndex);586break;587}588case GroupModelChangeKind.EDITOR_DIRTY:589if (event.editorIndex !== undefined && event.editor !== undefined) {590this._onDidTabDirty(groupId, event.editorIndex, event.editor);591break;592}593case GroupModelChangeKind.EDITOR_STICKY:594if (event.editorIndex !== undefined && event.editor !== undefined) {595this._onDidTabPinChange(groupId, event.editorIndex, event.editor);596break;597}598case GroupModelChangeKind.EDITOR_PIN:599if (event.editorIndex !== undefined && event.editor !== undefined) {600this._onDidTabPreviewChange(groupId, event.editorIndex, event.editor);601break;602}603case GroupModelChangeKind.EDITOR_TRANSIENT:604// Currently not exposed in the API605break;606case GroupModelChangeKind.EDITOR_MOVE:607if (isGroupEditorMoveEvent(event) && event.editor && event.editorIndex !== undefined && event.oldEditorIndex !== undefined) {608this._onDidTabMove(groupId, event.editorIndex, event.oldEditorIndex, event.editor);609break;610}611default:612// If it's not an optimized case we rebuild the tabs model from scratch613this._createTabsModel();614}615}616//#region Messages received from Ext Host617$moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void {618const groupId = columnToEditorGroup(this._editorGroupsService, this._configurationService, viewColumn);619const tabInfo = this._tabInfoLookup.get(tabId);620const tab = tabInfo?.tab;621if (!tab) {622throw new Error(`Attempted to close tab with id ${tabId} which does not exist`);623}624let targetGroup: IEditorGroup | undefined;625const sourceGroup = this._editorGroupsService.getGroup(tabInfo.group.id);626if (!sourceGroup) {627return;628}629// If group index is out of bounds then we make a new one that's to the right of the last group630if (this._groupLookup.get(groupId) === undefined) {631let direction = GroupDirection.RIGHT;632// Make sure we respect the user's preferred side direction633if (viewColumn === SIDE_GROUP) {634direction = preferredSideBySideGroupDirection(this._configurationService);635}636targetGroup = this._editorGroupsService.addGroup(this._editorGroupsService.groups[this._editorGroupsService.groups.length - 1], direction);637} else {638targetGroup = this._editorGroupsService.getGroup(groupId);639}640if (!targetGroup) {641return;642}643644// Similar logic to if index is out of bounds we place it at the end645if (index < 0 || index > targetGroup.editors.length) {646index = targetGroup.editors.length;647}648// Find the correct EditorInput using the tab info649const editorInput = tabInfo?.editorInput;650if (!editorInput) {651return;652}653// Move the editor to the target group654sourceGroup.moveEditor(editorInput, targetGroup, { index, preserveFocus });655return;656}657658async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean> {659const groups: Map<IEditorGroup, EditorInput[]> = new Map();660for (const tabId of tabIds) {661const tabInfo = this._tabInfoLookup.get(tabId);662const tab = tabInfo?.tab;663const group = tabInfo?.group;664const editorTab = tabInfo?.editorInput;665// If not found skip666if (!group || !tab || !tabInfo || !editorTab) {667continue;668}669const groupEditors = groups.get(group);670if (!groupEditors) {671groups.set(group, [editorTab]);672} else {673groupEditors.push(editorTab);674}675}676// Loop over keys of the groups map and call closeEditors677const results: boolean[] = [];678for (const [group, editors] of groups) {679results.push(await group.closeEditors(editors, { preserveFocus }));680}681// TODO @jrieken This isn't quite right how can we say true for some but not others?682return results.every(result => result);683}684685async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> {686const groupCloseResults: boolean[] = [];687for (const groupId of groupIds) {688const group = this._editorGroupsService.getGroup(groupId);689if (group) {690groupCloseResults.push(await group.closeAllEditors());691// Make sure group is empty but still there before removing it692if (group.count === 0 && this._editorGroupsService.getGroup(group.id)) {693this._editorGroupsService.removeGroup(group);694}695}696}697return groupCloseResults.every(result => result);698}699//#endregion700}701702703