Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostEditorTabs.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { diffSets } from '../../../base/common/collections.js';
7
import { Emitter } from '../../../base/common/event.js';
8
import { assertReturnsDefined } from '../../../base/common/types.js';
9
import { URI } from '../../../base/common/uri.js';
10
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
11
import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TabOperation } from './extHost.protocol.js';
12
import { IExtHostRpcService } from './extHostRpcService.js';
13
import * as typeConverters from './extHostTypeConverters.js';
14
import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput, TextMultiDiffTabInput } from './extHostTypes.js';
15
import type * as vscode from 'vscode';
16
17
export interface IExtHostEditorTabs extends IExtHostEditorTabsShape {
18
readonly _serviceBrand: undefined;
19
tabGroups: vscode.TabGroups;
20
}
21
22
export const IExtHostEditorTabs = createDecorator<IExtHostEditorTabs>('IExtHostEditorTabs');
23
24
type AnyTabInput = TextTabInput | TextDiffTabInput | TextMultiDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput;
25
26
class ExtHostEditorTab {
27
private _apiObject: vscode.Tab | undefined;
28
private _dto!: IEditorTabDto;
29
private _input: AnyTabInput | undefined;
30
private _parentGroup: ExtHostEditorTabGroup;
31
private readonly _activeTabIdGetter: () => string;
32
33
constructor(dto: IEditorTabDto, parentGroup: ExtHostEditorTabGroup, activeTabIdGetter: () => string) {
34
this._activeTabIdGetter = activeTabIdGetter;
35
this._parentGroup = parentGroup;
36
this.acceptDtoUpdate(dto);
37
}
38
39
get apiObject(): vscode.Tab {
40
if (!this._apiObject) {
41
// Don't want to lose reference to parent `this` in the getters
42
const that = this;
43
const obj: vscode.Tab = {
44
get isActive() {
45
// We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required
46
return that._dto.id === that._activeTabIdGetter();
47
},
48
get label() {
49
return that._dto.label;
50
},
51
get input() {
52
return that._input;
53
},
54
get isDirty() {
55
return that._dto.isDirty;
56
},
57
get isPinned() {
58
return that._dto.isPinned;
59
},
60
get isPreview() {
61
return that._dto.isPreview;
62
},
63
get group() {
64
return that._parentGroup.apiObject;
65
}
66
};
67
this._apiObject = Object.freeze<vscode.Tab>(obj);
68
}
69
return this._apiObject;
70
}
71
72
get tabId(): string {
73
return this._dto.id;
74
}
75
76
acceptDtoUpdate(dto: IEditorTabDto) {
77
this._dto = dto;
78
this._input = this._initInput();
79
}
80
81
private _initInput() {
82
switch (this._dto.input.kind) {
83
case TabInputKind.TextInput:
84
return new TextTabInput(URI.revive(this._dto.input.uri));
85
case TabInputKind.TextDiffInput:
86
return new TextDiffTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified));
87
case TabInputKind.TextMergeInput:
88
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));
89
case TabInputKind.CustomEditorInput:
90
return new CustomEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.viewType);
91
case TabInputKind.WebviewEditorInput:
92
return new WebviewEditorTabInput(this._dto.input.viewType);
93
case TabInputKind.NotebookInput:
94
return new NotebookEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.notebookType);
95
case TabInputKind.NotebookDiffInput:
96
return new NotebookDiffEditorTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified), this._dto.input.notebookType);
97
case TabInputKind.TerminalEditorInput:
98
return new TerminalEditorTabInput();
99
case TabInputKind.InteractiveEditorInput:
100
return new InteractiveWindowInput(URI.revive(this._dto.input.uri), URI.revive(this._dto.input.inputBoxUri));
101
case TabInputKind.ChatEditorInput:
102
return new ChatEditorTabInput();
103
case TabInputKind.MultiDiffEditorInput:
104
return new TextMultiDiffTabInput(this._dto.input.diffEditors.map(diff => new TextDiffTabInput(URI.revive(diff.original), URI.revive(diff.modified))));
105
default:
106
return undefined;
107
}
108
}
109
}
110
111
class ExtHostEditorTabGroup {
112
113
private _apiObject: vscode.TabGroup | undefined;
114
private _dto: IEditorTabGroupDto;
115
private _tabs: ExtHostEditorTab[] = [];
116
private _activeTabId: string = '';
117
private _activeGroupIdGetter: () => number | undefined;
118
119
constructor(dto: IEditorTabGroupDto, activeGroupIdGetter: () => number | undefined) {
120
this._dto = dto;
121
this._activeGroupIdGetter = activeGroupIdGetter;
122
// Construct all tabs from the given dto
123
for (const tabDto of dto.tabs) {
124
if (tabDto.isActive) {
125
this._activeTabId = tabDto.id;
126
}
127
this._tabs.push(new ExtHostEditorTab(tabDto, this, () => this.activeTabId()));
128
}
129
}
130
131
get apiObject(): vscode.TabGroup {
132
if (!this._apiObject) {
133
// Don't want to lose reference to parent `this` in the getters
134
const that = this;
135
const obj: vscode.TabGroup = {
136
get isActive() {
137
// We use a getter function here to always ensure at most 1 active group and prevent iteration for being required
138
return that._dto.groupId === that._activeGroupIdGetter();
139
},
140
get viewColumn() {
141
return typeConverters.ViewColumn.to(that._dto.viewColumn);
142
},
143
get activeTab() {
144
return that._tabs.find(tab => tab.tabId === that._activeTabId)?.apiObject;
145
},
146
get tabs() {
147
return Object.freeze(that._tabs.map(tab => tab.apiObject));
148
}
149
};
150
this._apiObject = Object.freeze<vscode.TabGroup>(obj);
151
}
152
return this._apiObject;
153
}
154
155
get groupId(): number {
156
return this._dto.groupId;
157
}
158
159
get tabs(): ExtHostEditorTab[] {
160
return this._tabs;
161
}
162
163
acceptGroupDtoUpdate(dto: IEditorTabGroupDto) {
164
this._dto = dto;
165
}
166
167
acceptTabOperation(operation: TabOperation): ExtHostEditorTab {
168
// In the open case we add the tab to the group
169
if (operation.kind === TabModelOperationKind.TAB_OPEN) {
170
const tab = new ExtHostEditorTab(operation.tabDto, this, () => this.activeTabId());
171
// Insert tab at editor index
172
this._tabs.splice(operation.index, 0, tab);
173
if (operation.tabDto.isActive) {
174
this._activeTabId = tab.tabId;
175
}
176
return tab;
177
} else if (operation.kind === TabModelOperationKind.TAB_CLOSE) {
178
const tab = this._tabs.splice(operation.index, 1)[0];
179
if (!tab) {
180
throw new Error(`Tab close updated received for index ${operation.index} which does not exist`);
181
}
182
if (tab.tabId === this._activeTabId) {
183
this._activeTabId = '';
184
}
185
return tab;
186
} else if (operation.kind === TabModelOperationKind.TAB_MOVE) {
187
if (operation.oldIndex === undefined) {
188
throw new Error('Invalid old index on move IPC');
189
}
190
// Splice to remove at old index and insert at new index === moving the tab
191
const tab = this._tabs.splice(operation.oldIndex, 1)[0];
192
if (!tab) {
193
throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`);
194
}
195
this._tabs.splice(operation.index, 0, tab);
196
return tab;
197
}
198
const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id);
199
if (!tab) {
200
throw new Error('INVALID tab');
201
}
202
if (operation.tabDto.isActive) {
203
this._activeTabId = operation.tabDto.id;
204
} else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) {
205
// Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id
206
// but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange from
207
// firing incorrectly
208
this._activeTabId = '';
209
}
210
tab.acceptDtoUpdate(operation.tabDto);
211
return tab;
212
}
213
214
// Not a getter since it must be a function to be used as a callback for the tabs
215
activeTabId(): string {
216
return this._activeTabId;
217
}
218
}
219
220
export class ExtHostEditorTabs implements IExtHostEditorTabs {
221
readonly _serviceBrand: undefined;
222
223
private readonly _proxy: MainThreadEditorTabsShape;
224
private readonly _onDidChangeTabs = new Emitter<vscode.TabChangeEvent>();
225
private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroupChangeEvent>();
226
227
// Have to use ! because this gets initialized via an RPC proxy
228
private _activeGroupId!: number;
229
230
private _extHostTabGroups: ExtHostEditorTabGroup[] = [];
231
232
private _apiObject: vscode.TabGroups | undefined;
233
234
constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) {
235
this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs);
236
}
237
238
get tabGroups(): vscode.TabGroups {
239
if (!this._apiObject) {
240
const that = this;
241
const obj: vscode.TabGroups = {
242
// never changes -> simple value
243
onDidChangeTabGroups: that._onDidChangeTabGroups.event,
244
onDidChangeTabs: that._onDidChangeTabs.event,
245
// dynamic -> getters
246
get all() {
247
return Object.freeze(that._extHostTabGroups.map(group => group.apiObject));
248
},
249
get activeTabGroup() {
250
const activeTabGroupId = that._activeGroupId;
251
const activeTabGroup = assertReturnsDefined(that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject);
252
return activeTabGroup;
253
},
254
close: async (tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], preserveFocus?: boolean) => {
255
const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup];
256
if (!tabsOrTabGroups.length) {
257
return true;
258
}
259
// Check which type was passed in and call the appropriate close
260
// Casting is needed as typescript doesn't seem to infer enough from this
261
if (isTabGroup(tabsOrTabGroups[0])) {
262
return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus);
263
} else {
264
return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus);
265
}
266
},
267
// move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => {
268
// const extHostTab = this._findExtHostTabFromApi(tab);
269
// if (!extHostTab) {
270
// throw new Error('Invalid tab');
271
// }
272
// this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus);
273
// return;
274
// }
275
};
276
this._apiObject = Object.freeze(obj);
277
}
278
return this._apiObject;
279
}
280
281
$acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void {
282
283
const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId));
284
const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId));
285
const diff = diffSets(groupIdsBefore, groupIdsAfter);
286
287
const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject);
288
const opened: vscode.TabGroup[] = [];
289
const changed: vscode.TabGroup[] = [];
290
291
292
this._extHostTabGroups = tabGroups.map(tabGroup => {
293
const group = new ExtHostEditorTabGroup(tabGroup, () => this._activeGroupId);
294
if (diff.added.includes(group.groupId)) {
295
opened.push(group.apiObject);
296
} else {
297
changed.push(group.apiObject);
298
}
299
return group;
300
});
301
302
// Set the active tab group id
303
const activeTabGroupId = assertReturnsDefined(tabGroups.find(group => group.isActive === true)?.groupId);
304
if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) {
305
this._activeGroupId = activeTabGroupId;
306
}
307
this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed }));
308
}
309
310
$acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) {
311
const group = this._extHostTabGroups.find(group => group.groupId === groupDto.groupId);
312
if (!group) {
313
throw new Error('Update Group IPC call received before group creation.');
314
}
315
group.acceptGroupDtoUpdate(groupDto);
316
if (groupDto.isActive) {
317
this._activeGroupId = groupDto.groupId;
318
}
319
this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] }));
320
}
321
322
$acceptTabOperation(operation: TabOperation) {
323
const group = this._extHostTabGroups.find(group => group.groupId === operation.groupId);
324
if (!group) {
325
throw new Error('Update Tabs IPC call received before group creation.');
326
}
327
const tab = group.acceptTabOperation(operation);
328
329
// Construct the tab change event based on the operation
330
switch (operation.kind) {
331
case TabModelOperationKind.TAB_OPEN:
332
this._onDidChangeTabs.fire(Object.freeze({
333
opened: [tab.apiObject],
334
closed: [],
335
changed: []
336
}));
337
return;
338
case TabModelOperationKind.TAB_CLOSE:
339
this._onDidChangeTabs.fire(Object.freeze({
340
opened: [],
341
closed: [tab.apiObject],
342
changed: []
343
}));
344
return;
345
case TabModelOperationKind.TAB_MOVE:
346
case TabModelOperationKind.TAB_UPDATE:
347
this._onDidChangeTabs.fire(Object.freeze({
348
opened: [],
349
closed: [],
350
changed: [tab.apiObject]
351
}));
352
return;
353
}
354
}
355
356
private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined {
357
for (const group of this._extHostTabGroups) {
358
for (const tab of group.tabs) {
359
if (tab.apiObject === apiTab) {
360
return tab;
361
}
362
}
363
}
364
return;
365
}
366
367
private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined {
368
return this._extHostTabGroups.find(candidate => candidate.apiObject === apiTabGroup);
369
}
370
371
private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise<boolean> {
372
const extHostTabIds: string[] = [];
373
for (const tab of tabs) {
374
const extHostTab = this._findExtHostTabFromApi(tab);
375
if (!extHostTab) {
376
throw new Error('Tab close: Invalid tab not found!');
377
}
378
extHostTabIds.push(extHostTab.tabId);
379
}
380
return this._proxy.$closeTab(extHostTabIds, preserveFocus);
381
}
382
383
private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise<boolean> {
384
const extHostGroupIds: number[] = [];
385
for (const group of groups) {
386
const extHostGroup = this._findExtHostTabGroupFromApi(group);
387
if (!extHostGroup) {
388
throw new Error('Group close: Invalid group not found!');
389
}
390
extHostGroupIds.push(extHostGroup.groupId);
391
}
392
return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs);
393
}
394
}
395
396
//#region Utils
397
function isTabGroup(obj: unknown): obj is vscode.TabGroup {
398
const tabGroup = obj as vscode.TabGroup;
399
if (tabGroup.tabs !== undefined) {
400
return true;
401
}
402
return false;
403
}
404
//#endregion
405
406