Path: blob/main/src/vs/workbench/api/common/extHostEditorTabs.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 { diffSets } from '../../../base/common/collections.js';6import { Emitter } from '../../../base/common/event.js';7import { assertReturnsDefined } from '../../../base/common/types.js';8import { URI } from '../../../base/common/uri.js';9import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';10import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TabOperation } from './extHost.protocol.js';11import { IExtHostRpcService } from './extHostRpcService.js';12import * as typeConverters from './extHostTypeConverters.js';13import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput, TextMultiDiffTabInput } from './extHostTypes.js';14import type * as vscode from 'vscode';1516export interface IExtHostEditorTabs extends IExtHostEditorTabsShape {17readonly _serviceBrand: undefined;18tabGroups: vscode.TabGroups;19}2021export const IExtHostEditorTabs = createDecorator<IExtHostEditorTabs>('IExtHostEditorTabs');2223type AnyTabInput = TextTabInput | TextDiffTabInput | TextMultiDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput;2425class ExtHostEditorTab {26private _apiObject: vscode.Tab | undefined;27private _dto!: IEditorTabDto;28private _input: AnyTabInput | undefined;29private _parentGroup: ExtHostEditorTabGroup;30private readonly _activeTabIdGetter: () => string;3132constructor(dto: IEditorTabDto, parentGroup: ExtHostEditorTabGroup, activeTabIdGetter: () => string) {33this._activeTabIdGetter = activeTabIdGetter;34this._parentGroup = parentGroup;35this.acceptDtoUpdate(dto);36}3738get apiObject(): vscode.Tab {39if (!this._apiObject) {40// Don't want to lose reference to parent `this` in the getters41const that = this;42const obj: vscode.Tab = {43get isActive() {44// We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required45return that._dto.id === that._activeTabIdGetter();46},47get label() {48return that._dto.label;49},50get input() {51return that._input;52},53get isDirty() {54return that._dto.isDirty;55},56get isPinned() {57return that._dto.isPinned;58},59get isPreview() {60return that._dto.isPreview;61},62get group() {63return that._parentGroup.apiObject;64}65};66this._apiObject = Object.freeze<vscode.Tab>(obj);67}68return this._apiObject;69}7071get tabId(): string {72return this._dto.id;73}7475acceptDtoUpdate(dto: IEditorTabDto) {76this._dto = dto;77this._input = this._initInput();78}7980private _initInput() {81switch (this._dto.input.kind) {82case TabInputKind.TextInput:83return new TextTabInput(URI.revive(this._dto.input.uri));84case TabInputKind.TextDiffInput:85return new TextDiffTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified));86case TabInputKind.TextMergeInput:87return 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));88case TabInputKind.CustomEditorInput:89return new CustomEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.viewType);90case TabInputKind.WebviewEditorInput:91return new WebviewEditorTabInput(this._dto.input.viewType);92case TabInputKind.NotebookInput:93return new NotebookEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.notebookType);94case TabInputKind.NotebookDiffInput:95return new NotebookDiffEditorTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified), this._dto.input.notebookType);96case TabInputKind.TerminalEditorInput:97return new TerminalEditorTabInput();98case TabInputKind.InteractiveEditorInput:99return new InteractiveWindowInput(URI.revive(this._dto.input.uri), URI.revive(this._dto.input.inputBoxUri));100case TabInputKind.ChatEditorInput:101return new ChatEditorTabInput();102case TabInputKind.MultiDiffEditorInput:103return new TextMultiDiffTabInput(this._dto.input.diffEditors.map(diff => new TextDiffTabInput(URI.revive(diff.original), URI.revive(diff.modified))));104default:105return undefined;106}107}108}109110class ExtHostEditorTabGroup {111112private _apiObject: vscode.TabGroup | undefined;113private _dto: IEditorTabGroupDto;114private _tabs: ExtHostEditorTab[] = [];115private _activeTabId: string = '';116private _activeGroupIdGetter: () => number | undefined;117118constructor(dto: IEditorTabGroupDto, activeGroupIdGetter: () => number | undefined) {119this._dto = dto;120this._activeGroupIdGetter = activeGroupIdGetter;121// Construct all tabs from the given dto122for (const tabDto of dto.tabs) {123if (tabDto.isActive) {124this._activeTabId = tabDto.id;125}126this._tabs.push(new ExtHostEditorTab(tabDto, this, () => this.activeTabId()));127}128}129130get apiObject(): vscode.TabGroup {131if (!this._apiObject) {132// Don't want to lose reference to parent `this` in the getters133const that = this;134const obj: vscode.TabGroup = {135get isActive() {136// We use a getter function here to always ensure at most 1 active group and prevent iteration for being required137return that._dto.groupId === that._activeGroupIdGetter();138},139get viewColumn() {140return typeConverters.ViewColumn.to(that._dto.viewColumn);141},142get activeTab() {143return that._tabs.find(tab => tab.tabId === that._activeTabId)?.apiObject;144},145get tabs() {146return Object.freeze(that._tabs.map(tab => tab.apiObject));147}148};149this._apiObject = Object.freeze<vscode.TabGroup>(obj);150}151return this._apiObject;152}153154get groupId(): number {155return this._dto.groupId;156}157158get tabs(): ExtHostEditorTab[] {159return this._tabs;160}161162acceptGroupDtoUpdate(dto: IEditorTabGroupDto) {163this._dto = dto;164}165166acceptTabOperation(operation: TabOperation): ExtHostEditorTab {167// In the open case we add the tab to the group168if (operation.kind === TabModelOperationKind.TAB_OPEN) {169const tab = new ExtHostEditorTab(operation.tabDto, this, () => this.activeTabId());170// Insert tab at editor index171this._tabs.splice(operation.index, 0, tab);172if (operation.tabDto.isActive) {173this._activeTabId = tab.tabId;174}175return tab;176} else if (operation.kind === TabModelOperationKind.TAB_CLOSE) {177const tab = this._tabs.splice(operation.index, 1)[0];178if (!tab) {179throw new Error(`Tab close updated received for index ${operation.index} which does not exist`);180}181if (tab.tabId === this._activeTabId) {182this._activeTabId = '';183}184return tab;185} else if (operation.kind === TabModelOperationKind.TAB_MOVE) {186if (operation.oldIndex === undefined) {187throw new Error('Invalid old index on move IPC');188}189// Splice to remove at old index and insert at new index === moving the tab190const tab = this._tabs.splice(operation.oldIndex, 1)[0];191if (!tab) {192throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`);193}194this._tabs.splice(operation.index, 0, tab);195return tab;196}197const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id);198if (!tab) {199throw new Error('INVALID tab');200}201if (operation.tabDto.isActive) {202this._activeTabId = operation.tabDto.id;203} else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) {204// Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id205// but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange from206// firing incorrectly207this._activeTabId = '';208}209tab.acceptDtoUpdate(operation.tabDto);210return tab;211}212213// Not a getter since it must be a function to be used as a callback for the tabs214activeTabId(): string {215return this._activeTabId;216}217}218219export class ExtHostEditorTabs implements IExtHostEditorTabs {220readonly _serviceBrand: undefined;221222private readonly _proxy: MainThreadEditorTabsShape;223private readonly _onDidChangeTabs = new Emitter<vscode.TabChangeEvent>();224private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroupChangeEvent>();225226// Have to use ! because this gets initialized via an RPC proxy227private _activeGroupId!: number;228229private _extHostTabGroups: ExtHostEditorTabGroup[] = [];230231private _apiObject: vscode.TabGroups | undefined;232233constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) {234this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs);235}236237get tabGroups(): vscode.TabGroups {238if (!this._apiObject) {239const that = this;240const obj: vscode.TabGroups = {241// never changes -> simple value242onDidChangeTabGroups: that._onDidChangeTabGroups.event,243onDidChangeTabs: that._onDidChangeTabs.event,244// dynamic -> getters245get all() {246return Object.freeze(that._extHostTabGroups.map(group => group.apiObject));247},248get activeTabGroup() {249const activeTabGroupId = that._activeGroupId;250const activeTabGroup = assertReturnsDefined(that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject);251return activeTabGroup;252},253close: async (tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], preserveFocus?: boolean) => {254const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup];255if (!tabsOrTabGroups.length) {256return true;257}258// Check which type was passed in and call the appropriate close259// Casting is needed as typescript doesn't seem to infer enough from this260if (isTabGroup(tabsOrTabGroups[0])) {261return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus);262} else {263return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus);264}265},266// move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => {267// const extHostTab = this._findExtHostTabFromApi(tab);268// if (!extHostTab) {269// throw new Error('Invalid tab');270// }271// this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus);272// return;273// }274};275this._apiObject = Object.freeze(obj);276}277return this._apiObject;278}279280$acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void {281282const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId));283const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId));284const diff = diffSets(groupIdsBefore, groupIdsAfter);285286const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject);287const opened: vscode.TabGroup[] = [];288const changed: vscode.TabGroup[] = [];289290291this._extHostTabGroups = tabGroups.map(tabGroup => {292const group = new ExtHostEditorTabGroup(tabGroup, () => this._activeGroupId);293if (diff.added.includes(group.groupId)) {294opened.push(group.apiObject);295} else {296changed.push(group.apiObject);297}298return group;299});300301// Set the active tab group id302const activeTabGroupId = assertReturnsDefined(tabGroups.find(group => group.isActive === true)?.groupId);303if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) {304this._activeGroupId = activeTabGroupId;305}306this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed }));307}308309$acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) {310const group = this._extHostTabGroups.find(group => group.groupId === groupDto.groupId);311if (!group) {312throw new Error('Update Group IPC call received before group creation.');313}314group.acceptGroupDtoUpdate(groupDto);315if (groupDto.isActive) {316this._activeGroupId = groupDto.groupId;317}318this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] }));319}320321$acceptTabOperation(operation: TabOperation) {322const group = this._extHostTabGroups.find(group => group.groupId === operation.groupId);323if (!group) {324throw new Error('Update Tabs IPC call received before group creation.');325}326const tab = group.acceptTabOperation(operation);327328// Construct the tab change event based on the operation329switch (operation.kind) {330case TabModelOperationKind.TAB_OPEN:331this._onDidChangeTabs.fire(Object.freeze({332opened: [tab.apiObject],333closed: [],334changed: []335}));336return;337case TabModelOperationKind.TAB_CLOSE:338this._onDidChangeTabs.fire(Object.freeze({339opened: [],340closed: [tab.apiObject],341changed: []342}));343return;344case TabModelOperationKind.TAB_MOVE:345case TabModelOperationKind.TAB_UPDATE:346this._onDidChangeTabs.fire(Object.freeze({347opened: [],348closed: [],349changed: [tab.apiObject]350}));351return;352}353}354355private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined {356for (const group of this._extHostTabGroups) {357for (const tab of group.tabs) {358if (tab.apiObject === apiTab) {359return tab;360}361}362}363return;364}365366private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined {367return this._extHostTabGroups.find(candidate => candidate.apiObject === apiTabGroup);368}369370private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise<boolean> {371const extHostTabIds: string[] = [];372for (const tab of tabs) {373const extHostTab = this._findExtHostTabFromApi(tab);374if (!extHostTab) {375throw new Error('Tab close: Invalid tab not found!');376}377extHostTabIds.push(extHostTab.tabId);378}379return this._proxy.$closeTab(extHostTabIds, preserveFocus);380}381382private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise<boolean> {383const extHostGroupIds: number[] = [];384for (const group of groups) {385const extHostGroup = this._findExtHostTabGroupFromApi(group);386if (!extHostGroup) {387throw new Error('Group close: Invalid group not found!');388}389extHostGroupIds.push(extHostGroup.groupId);390}391return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs);392}393}394395//#region Utils396function isTabGroup(obj: unknown): obj is vscode.TabGroup {397const tabGroup = obj as vscode.TabGroup;398if (tabGroup.tabs !== undefined) {399return true;400}401return false;402}403//#endregion404405406