Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadEditorTabs.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 { Event } from '../../../base/common/event.js';
7
import { DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';
8
import { isEqual } from '../../../base/common/resources.js';
9
import { URI } from '../../../base/common/uri.js';
10
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
11
import { ILogService } from '../../../platform/log/common/log.js';
12
import { AnyInputDto, ExtHostContext, IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextDiffInputDto } from '../common/extHost.protocol.js';
13
import { EditorResourceAccessor, GroupModelChangeKind, SideBySideEditor } from '../../common/editor.js';
14
import { DiffEditorInput } from '../../common/editor/diffEditorInput.js';
15
import { isGroupEditorMoveEvent } from '../../common/editor/editorGroupModel.js';
16
import { EditorInput } from '../../common/editor/editorInput.js';
17
import { SideBySideEditorInput } from '../../common/editor/sideBySideEditorInput.js';
18
import { AbstractTextResourceEditorInput } from '../../common/editor/textResourceEditorInput.js';
19
import { ChatEditorInput } from '../../contrib/chat/browser/chatEditorInput.js';
20
import { CustomEditorInput } from '../../contrib/customEditor/browser/customEditorInput.js';
21
import { InteractiveEditorInput } from '../../contrib/interactive/browser/interactiveEditorInput.js';
22
import { MergeEditorInput } from '../../contrib/mergeEditor/browser/mergeEditorInput.js';
23
import { MultiDiffEditorInput } from '../../contrib/multiDiffEditor/browser/multiDiffEditorInput.js';
24
import { NotebookEditorInput } from '../../contrib/notebook/common/notebookEditorInput.js';
25
import { TerminalEditorInput } from '../../contrib/terminal/browser/terminalEditorInput.js';
26
import { WebviewInput } from '../../contrib/webviewPanel/browser/webviewEditorInput.js';
27
import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from '../../services/editor/common/editorGroupColumn.js';
28
import { GroupDirection, IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from '../../services/editor/common/editorGroupsService.js';
29
import { IEditorsChangeEvent, IEditorService, SIDE_GROUP } from '../../services/editor/common/editorService.js';
30
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
31
32
interface TabInfo {
33
tab: IEditorTabDto;
34
group: IEditorGroup;
35
editorInput: EditorInput;
36
}
37
@extHostNamedCustomer(MainContext.MainThreadEditorTabs)
38
export class MainThreadEditorTabs implements MainThreadEditorTabsShape {
39
40
private readonly _dispoables = new DisposableStore();
41
private readonly _proxy: IExtHostEditorTabsShape;
42
// List of all groups and their corresponding tabs, this is **the** model
43
private _tabGroupModel: IEditorTabGroupDto[] = [];
44
// Lookup table for finding group by id
45
private readonly _groupLookup: Map<number, IEditorTabGroupDto> = new Map();
46
// Lookup table for finding tab by id
47
private readonly _tabInfoLookup: Map<string, TabInfo> = new Map();
48
// Tracks the currently open MultiDiffEditorInputs to listen to resource changes
49
private readonly _multiDiffEditorInputListeners: DisposableMap<MultiDiffEditorInput> = new DisposableMap();
50
51
constructor(
52
extHostContext: IExtHostContext,
53
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
54
@IConfigurationService private readonly _configurationService: IConfigurationService,
55
@ILogService private readonly _logService: ILogService,
56
@IEditorService editorService: IEditorService
57
) {
58
59
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);
60
61
// Main listener which responds to events from the editor service
62
this._dispoables.add(editorService.onDidEditorsChange((event) => {
63
try {
64
this._updateTabsModel(event);
65
} catch {
66
this._logService.error('Failed to update model, rebuilding');
67
this._createTabsModel();
68
}
69
}));
70
71
this._dispoables.add(this._multiDiffEditorInputListeners);
72
73
// Structural group changes (add, remove, move, etc) are difficult to patch.
74
// Since they happen infrequently we just rebuild the entire model
75
this._dispoables.add(this._editorGroupsService.onDidAddGroup(() => this._createTabsModel()));
76
this._dispoables.add(this._editorGroupsService.onDidRemoveGroup(() => this._createTabsModel()));
77
78
// Once everything is read go ahead and initialize the model
79
this._editorGroupsService.whenReady.then(() => this._createTabsModel());
80
}
81
82
dispose(): void {
83
this._groupLookup.clear();
84
this._tabInfoLookup.clear();
85
this._dispoables.dispose();
86
}
87
88
/**
89
* Creates a tab object with the correct properties
90
* @param editor The editor input represented by the tab
91
* @param group The group the tab is in
92
* @returns A tab object
93
*/
94
private _buildTabObject(group: IEditorGroup, editor: EditorInput, editorIndex: number): IEditorTabDto {
95
const editorId = editor.editorId;
96
const tab: IEditorTabDto = {
97
id: this._generateTabId(editor, group.id),
98
label: editor.getName(),
99
editorId,
100
input: this._editorInputToDto(editor),
101
isPinned: group.isSticky(editorIndex),
102
isPreview: !group.isPinned(editorIndex),
103
isActive: group.isActive(editor),
104
isDirty: editor.isDirty()
105
};
106
return tab;
107
}
108
109
private _editorInputToDto(editor: EditorInput): AnyInputDto {
110
111
if (editor instanceof MergeEditorInput) {
112
return {
113
kind: TabInputKind.TextMergeInput,
114
base: editor.base,
115
input1: editor.input1.uri,
116
input2: editor.input2.uri,
117
result: editor.resource
118
};
119
}
120
121
if (editor instanceof AbstractTextResourceEditorInput) {
122
return {
123
kind: TabInputKind.TextInput,
124
uri: editor.resource
125
};
126
}
127
128
if (editor instanceof SideBySideEditorInput && !(editor instanceof DiffEditorInput)) {
129
const primaryResource = editor.primary.resource;
130
const secondaryResource = editor.secondary.resource;
131
// If side by side editor with same resource on both sides treat it as a singular tab kind
132
if (editor.primary instanceof AbstractTextResourceEditorInput
133
&& editor.secondary instanceof AbstractTextResourceEditorInput
134
&& isEqual(primaryResource, secondaryResource)
135
&& primaryResource
136
&& secondaryResource
137
) {
138
return {
139
kind: TabInputKind.TextInput,
140
uri: primaryResource
141
};
142
}
143
return { kind: TabInputKind.UnknownInput };
144
}
145
146
if (editor instanceof NotebookEditorInput) {
147
return {
148
kind: TabInputKind.NotebookInput,
149
notebookType: editor.viewType,
150
uri: editor.resource
151
};
152
}
153
154
if (editor instanceof CustomEditorInput) {
155
return {
156
kind: TabInputKind.CustomEditorInput,
157
viewType: editor.viewType,
158
uri: editor.resource,
159
};
160
}
161
162
if (editor instanceof WebviewInput) {
163
return {
164
kind: TabInputKind.WebviewEditorInput,
165
viewType: editor.viewType
166
};
167
}
168
169
if (editor instanceof TerminalEditorInput) {
170
return {
171
kind: TabInputKind.TerminalEditorInput
172
};
173
}
174
175
if (editor instanceof DiffEditorInput) {
176
if (editor.modified instanceof AbstractTextResourceEditorInput && editor.original instanceof AbstractTextResourceEditorInput) {
177
return {
178
kind: TabInputKind.TextDiffInput,
179
modified: editor.modified.resource,
180
original: editor.original.resource
181
};
182
}
183
if (editor.modified instanceof NotebookEditorInput && editor.original instanceof NotebookEditorInput) {
184
return {
185
kind: TabInputKind.NotebookDiffInput,
186
notebookType: editor.original.viewType,
187
modified: editor.modified.resource,
188
original: editor.original.resource
189
};
190
}
191
}
192
193
if (editor instanceof InteractiveEditorInput) {
194
return {
195
kind: TabInputKind.InteractiveEditorInput,
196
uri: editor.resource,
197
inputBoxUri: editor.inputResource
198
};
199
}
200
201
if (editor instanceof ChatEditorInput) {
202
return {
203
kind: TabInputKind.ChatEditorInput,
204
};
205
}
206
207
if (editor instanceof MultiDiffEditorInput) {
208
const diffEditors: TextDiffInputDto[] = [];
209
for (const resource of (editor?.resources.get() ?? [])) {
210
if (resource.originalUri && resource.modifiedUri) {
211
diffEditors.push({
212
kind: TabInputKind.TextDiffInput,
213
original: resource.originalUri,
214
modified: resource.modifiedUri
215
});
216
}
217
}
218
219
return {
220
kind: TabInputKind.MultiDiffEditorInput,
221
diffEditors
222
};
223
}
224
225
return { kind: TabInputKind.UnknownInput };
226
}
227
228
/**
229
* Generates a unique id for a tab
230
* @param editor The editor input
231
* @param groupId The group id
232
* @returns A unique identifier for a specific tab
233
*/
234
private _generateTabId(editor: EditorInput, groupId: number) {
235
let resourceString: string | undefined;
236
// Properly get the resource and account for side by side editors
237
const resource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.BOTH });
238
if (resource instanceof URI) {
239
resourceString = resource.toString();
240
} else {
241
resourceString = `${resource?.primary?.toString()}-${resource?.secondary?.toString()}`;
242
}
243
return `${groupId}~${editor.editorId}-${editor.typeId}-${resourceString} `;
244
}
245
246
/**
247
* Called whenever a group activates, updates the model by marking the group as active an notifies the extension host
248
*/
249
private _onDidGroupActivate() {
250
const activeGroupId = this._editorGroupsService.activeGroup.id;
251
const activeGroup = this._groupLookup.get(activeGroupId);
252
if (activeGroup) {
253
// Ok not to loop as exthost accepts last active group
254
activeGroup.isActive = true;
255
this._proxy.$acceptTabGroupUpdate(activeGroup);
256
}
257
}
258
259
/**
260
* Called when the tab label changes
261
* @param groupId The id of the group the tab exists in
262
* @param editorInput The editor input represented by the tab
263
*/
264
private _onDidTabLabelChange(groupId: number, editorInput: EditorInput, editorIndex: number) {
265
const tabId = this._generateTabId(editorInput, groupId);
266
const tabInfo = this._tabInfoLookup.get(tabId);
267
// If tab is found patch, else rebuild
268
if (tabInfo) {
269
tabInfo.tab.label = editorInput.getName();
270
this._proxy.$acceptTabOperation({
271
groupId,
272
index: editorIndex,
273
tabDto: tabInfo.tab,
274
kind: TabModelOperationKind.TAB_UPDATE
275
});
276
} else {
277
this._logService.error('Invalid model for label change, rebuilding');
278
this._createTabsModel();
279
}
280
}
281
282
/**
283
* Called when a new tab is opened
284
* @param groupId The id of the group the tab is being created in
285
* @param editorInput The editor input being opened
286
* @param editorIndex The index of the editor within that group
287
*/
288
private _onDidTabOpen(groupId: number, editorInput: EditorInput, editorIndex: number) {
289
const group = this._editorGroupsService.getGroup(groupId);
290
// Even if the editor service knows about the group the group might not exist yet in our model
291
const groupInModel = this._groupLookup.get(groupId) !== undefined;
292
// Means a new group was likely created so we rebuild the model
293
if (!group || !groupInModel) {
294
this._createTabsModel();
295
return;
296
}
297
const tabs = this._groupLookup.get(groupId)?.tabs;
298
if (!tabs) {
299
return;
300
}
301
// Splice tab into group at index editorIndex
302
const tabObject = this._buildTabObject(group, editorInput, editorIndex);
303
tabs.splice(editorIndex, 0, tabObject);
304
// Update lookup
305
const tabId = this._generateTabId(editorInput, groupId);
306
this._tabInfoLookup.set(tabId, { group, editorInput, tab: tabObject });
307
308
if (editorInput instanceof MultiDiffEditorInput) {
309
this._multiDiffEditorInputListeners.set(editorInput, Event.fromObservableLight(editorInput.resources)(() => {
310
const tabInfo = this._tabInfoLookup.get(tabId);
311
if (!tabInfo) {
312
return;
313
}
314
tabInfo.tab = this._buildTabObject(group, editorInput, editorIndex);
315
this._proxy.$acceptTabOperation({
316
groupId,
317
index: editorIndex,
318
tabDto: tabInfo.tab,
319
kind: TabModelOperationKind.TAB_UPDATE
320
});
321
}));
322
}
323
324
this._proxy.$acceptTabOperation({
325
groupId,
326
index: editorIndex,
327
tabDto: tabObject,
328
kind: TabModelOperationKind.TAB_OPEN
329
});
330
}
331
332
/**
333
* Called when a tab is closed
334
* @param groupId The id of the group the tab is being removed from
335
* @param editorIndex The index of the editor within that group
336
*/
337
private _onDidTabClose(groupId: number, editorIndex: number) {
338
const group = this._editorGroupsService.getGroup(groupId);
339
const tabs = this._groupLookup.get(groupId)?.tabs;
340
// Something is wrong with the model state so we rebuild
341
if (!group || !tabs) {
342
this._createTabsModel();
343
return;
344
}
345
// Splice tab into group at index editorIndex
346
const removedTab = tabs.splice(editorIndex, 1);
347
348
// Index must no longer be valid so we return prematurely
349
if (removedTab.length === 0) {
350
return;
351
}
352
353
// Update lookup
354
this._tabInfoLookup.delete(removedTab[0]?.id ?? '');
355
356
if (removedTab[0]?.input instanceof MultiDiffEditorInput) {
357
this._multiDiffEditorInputListeners.deleteAndDispose(removedTab[0]?.input);
358
}
359
360
this._proxy.$acceptTabOperation({
361
groupId,
362
index: editorIndex,
363
tabDto: removedTab[0],
364
kind: TabModelOperationKind.TAB_CLOSE
365
});
366
}
367
368
/**
369
* Called when the active tab changes
370
* @param groupId The id of the group the tab is contained in
371
* @param editorIndex The index of the tab
372
*/
373
private _onDidTabActiveChange(groupId: number, editorIndex: number) {
374
// TODO @lramos15 use the tab lookup here if possible. Do we have an editor input?!
375
const tabs = this._groupLookup.get(groupId)?.tabs;
376
if (!tabs) {
377
return;
378
}
379
const activeTab = tabs[editorIndex];
380
// No need to loop over as the exthost uses the most recently marked active tab
381
activeTab.isActive = true;
382
// Send DTO update to the exthost
383
this._proxy.$acceptTabOperation({
384
groupId,
385
index: editorIndex,
386
tabDto: activeTab,
387
kind: TabModelOperationKind.TAB_UPDATE
388
});
389
390
}
391
392
/**
393
* Called when the dirty indicator on the tab changes
394
* @param groupId The id of the group the tab is in
395
* @param editorIndex The index of the tab
396
* @param editor The editor input represented by the tab
397
*/
398
private _onDidTabDirty(groupId: number, editorIndex: number, editor: EditorInput) {
399
const tabId = this._generateTabId(editor, groupId);
400
const tabInfo = this._tabInfoLookup.get(tabId);
401
// Something wrong with the model state so we rebuild
402
if (!tabInfo) {
403
this._logService.error('Invalid model for dirty change, rebuilding');
404
this._createTabsModel();
405
return;
406
}
407
tabInfo.tab.isDirty = editor.isDirty();
408
this._proxy.$acceptTabOperation({
409
groupId,
410
index: editorIndex,
411
tabDto: tabInfo.tab,
412
kind: TabModelOperationKind.TAB_UPDATE
413
});
414
}
415
416
/**
417
* Called when the tab is pinned/unpinned
418
* @param groupId The id of the group the tab is in
419
* @param editorIndex The index of the tab
420
* @param editor The editor input represented by the tab
421
*/
422
private _onDidTabPinChange(groupId: number, editorIndex: number, editor: EditorInput) {
423
const tabId = this._generateTabId(editor, groupId);
424
const tabInfo = this._tabInfoLookup.get(tabId);
425
const group = tabInfo?.group;
426
const tab = tabInfo?.tab;
427
// Something wrong with the model state so we rebuild
428
if (!group || !tab) {
429
this._logService.error('Invalid model for sticky change, rebuilding');
430
this._createTabsModel();
431
return;
432
}
433
// Whether or not the tab has the pin icon (internally it's called sticky)
434
tab.isPinned = group.isSticky(editorIndex);
435
this._proxy.$acceptTabOperation({
436
groupId,
437
index: editorIndex,
438
tabDto: tab,
439
kind: TabModelOperationKind.TAB_UPDATE
440
});
441
}
442
443
/**
444
* Called when the tab is preview / unpreviewed
445
* @param groupId The id of the group the tab is in
446
* @param editorIndex The index of the tab
447
* @param editor The editor input represented by the tab
448
*/
449
private _onDidTabPreviewChange(groupId: number, editorIndex: number, editor: EditorInput) {
450
const tabId = this._generateTabId(editor, groupId);
451
const tabInfo = this._tabInfoLookup.get(tabId);
452
const group = tabInfo?.group;
453
const tab = tabInfo?.tab;
454
// Something wrong with the model state so we rebuild
455
if (!group || !tab) {
456
this._logService.error('Invalid model for sticky change, rebuilding');
457
this._createTabsModel();
458
return;
459
}
460
// Whether or not the tab has the pin icon (internally it's called pinned)
461
tab.isPreview = !group.isPinned(editorIndex);
462
this._proxy.$acceptTabOperation({
463
kind: TabModelOperationKind.TAB_UPDATE,
464
groupId,
465
tabDto: tab,
466
index: editorIndex
467
});
468
}
469
470
private _onDidTabMove(groupId: number, editorIndex: number, oldEditorIndex: number, editor: EditorInput) {
471
const tabs = this._groupLookup.get(groupId)?.tabs;
472
// Something wrong with the model state so we rebuild
473
if (!tabs) {
474
this._logService.error('Invalid model for move change, rebuilding');
475
this._createTabsModel();
476
return;
477
}
478
479
// Move tab from old index to new index
480
const removedTab = tabs.splice(oldEditorIndex, 1);
481
if (removedTab.length === 0) {
482
return;
483
}
484
tabs.splice(editorIndex, 0, removedTab[0]);
485
486
// Notify exthost of move
487
this._proxy.$acceptTabOperation({
488
kind: TabModelOperationKind.TAB_MOVE,
489
groupId,
490
tabDto: removedTab[0],
491
index: editorIndex,
492
oldIndex: oldEditorIndex
493
});
494
}
495
496
/**
497
* Builds the model from scratch based on the current state of the editor service.
498
*/
499
private _createTabsModel(): void {
500
if (this._editorGroupsService.groups.length === 0) {
501
return; // skip this invalid state, it may happen when the entire editor area is transitioning to other state ("editor working sets")
502
}
503
504
this._tabGroupModel = [];
505
this._groupLookup.clear();
506
this._tabInfoLookup.clear();
507
let tabs: IEditorTabDto[] = [];
508
for (const group of this._editorGroupsService.groups) {
509
const currentTabGroupModel: IEditorTabGroupDto = {
510
groupId: group.id,
511
isActive: group.id === this._editorGroupsService.activeGroup.id,
512
viewColumn: editorGroupToColumn(this._editorGroupsService, group),
513
tabs: []
514
};
515
group.editors.forEach((editor, editorIndex) => {
516
const tab = this._buildTabObject(group, editor, editorIndex);
517
tabs.push(tab);
518
// Add information about the tab to the lookup
519
this._tabInfoLookup.set(this._generateTabId(editor, group.id), {
520
group,
521
tab,
522
editorInput: editor
523
});
524
});
525
currentTabGroupModel.tabs = tabs;
526
this._tabGroupModel.push(currentTabGroupModel);
527
this._groupLookup.set(group.id, currentTabGroupModel);
528
tabs = [];
529
}
530
// notify the ext host of the new model
531
this._proxy.$acceptEditorTabModel(this._tabGroupModel);
532
}
533
534
// TODOD @lramos15 Remove this after done finishing the tab model code
535
// private _eventToString(event: IEditorsChangeEvent | IEditorsMoveEvent): string {
536
// let eventString = '';
537
// switch (event.kind) {
538
// case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX'; break;
539
// case GroupModelChangeKind.EDITOR_ACTIVE: eventString += 'EDITOR_ACTIVE'; break;
540
// case GroupModelChangeKind.EDITOR_PIN: eventString += 'EDITOR_PIN'; break;
541
// case GroupModelChangeKind.EDITOR_OPEN: eventString += 'EDITOR_OPEN'; break;
542
// case GroupModelChangeKind.EDITOR_CLOSE: eventString += 'EDITOR_CLOSE'; break;
543
// case GroupModelChangeKind.EDITOR_MOVE: eventString += 'EDITOR_MOVE'; break;
544
// case GroupModelChangeKind.EDITOR_LABEL: eventString += 'EDITOR_LABEL'; break;
545
// case GroupModelChangeKind.GROUP_ACTIVE: eventString += 'GROUP_ACTIVE'; break;
546
// case GroupModelChangeKind.GROUP_LOCKED: eventString += 'GROUP_LOCKED'; break;
547
// case GroupModelChangeKind.EDITOR_DIRTY: eventString += 'EDITOR_DIRTY'; break;
548
// case GroupModelChangeKind.EDITOR_STICKY: eventString += 'EDITOR_STICKY'; break;
549
// default: eventString += `UNKNOWN: ${event.kind}`; break;
550
// }
551
// return eventString;
552
// }
553
554
/**
555
* The main handler for the tab events
556
* @param events The list of events to process
557
*/
558
private _updateTabsModel(changeEvent: IEditorsChangeEvent): void {
559
const event = changeEvent.event;
560
const groupId = changeEvent.groupId;
561
switch (event.kind) {
562
case GroupModelChangeKind.GROUP_ACTIVE:
563
if (groupId === this._editorGroupsService.activeGroup.id) {
564
this._onDidGroupActivate();
565
break;
566
} else {
567
return;
568
}
569
case GroupModelChangeKind.EDITOR_LABEL:
570
if (event.editor !== undefined && event.editorIndex !== undefined) {
571
this._onDidTabLabelChange(groupId, event.editor, event.editorIndex);
572
break;
573
}
574
case GroupModelChangeKind.EDITOR_OPEN:
575
if (event.editor !== undefined && event.editorIndex !== undefined) {
576
this._onDidTabOpen(groupId, event.editor, event.editorIndex);
577
break;
578
}
579
case GroupModelChangeKind.EDITOR_CLOSE:
580
if (event.editorIndex !== undefined) {
581
this._onDidTabClose(groupId, event.editorIndex);
582
break;
583
}
584
case GroupModelChangeKind.EDITOR_ACTIVE:
585
if (event.editorIndex !== undefined) {
586
this._onDidTabActiveChange(groupId, event.editorIndex);
587
break;
588
}
589
case GroupModelChangeKind.EDITOR_DIRTY:
590
if (event.editorIndex !== undefined && event.editor !== undefined) {
591
this._onDidTabDirty(groupId, event.editorIndex, event.editor);
592
break;
593
}
594
case GroupModelChangeKind.EDITOR_STICKY:
595
if (event.editorIndex !== undefined && event.editor !== undefined) {
596
this._onDidTabPinChange(groupId, event.editorIndex, event.editor);
597
break;
598
}
599
case GroupModelChangeKind.EDITOR_PIN:
600
if (event.editorIndex !== undefined && event.editor !== undefined) {
601
this._onDidTabPreviewChange(groupId, event.editorIndex, event.editor);
602
break;
603
}
604
case GroupModelChangeKind.EDITOR_TRANSIENT:
605
// Currently not exposed in the API
606
break;
607
case GroupModelChangeKind.EDITOR_MOVE:
608
if (isGroupEditorMoveEvent(event) && event.editor && event.editorIndex !== undefined && event.oldEditorIndex !== undefined) {
609
this._onDidTabMove(groupId, event.editorIndex, event.oldEditorIndex, event.editor);
610
break;
611
}
612
default:
613
// If it's not an optimized case we rebuild the tabs model from scratch
614
this._createTabsModel();
615
}
616
}
617
//#region Messages received from Ext Host
618
$moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void {
619
const groupId = columnToEditorGroup(this._editorGroupsService, this._configurationService, viewColumn);
620
const tabInfo = this._tabInfoLookup.get(tabId);
621
const tab = tabInfo?.tab;
622
if (!tab) {
623
throw new Error(`Attempted to close tab with id ${tabId} which does not exist`);
624
}
625
let targetGroup: IEditorGroup | undefined;
626
const sourceGroup = this._editorGroupsService.getGroup(tabInfo.group.id);
627
if (!sourceGroup) {
628
return;
629
}
630
// If group index is out of bounds then we make a new one that's to the right of the last group
631
if (this._groupLookup.get(groupId) === undefined) {
632
let direction = GroupDirection.RIGHT;
633
// Make sure we respect the user's preferred side direction
634
if (viewColumn === SIDE_GROUP) {
635
direction = preferredSideBySideGroupDirection(this._configurationService);
636
}
637
targetGroup = this._editorGroupsService.addGroup(this._editorGroupsService.groups[this._editorGroupsService.groups.length - 1], direction);
638
} else {
639
targetGroup = this._editorGroupsService.getGroup(groupId);
640
}
641
if (!targetGroup) {
642
return;
643
}
644
645
// Similar logic to if index is out of bounds we place it at the end
646
if (index < 0 || index > targetGroup.editors.length) {
647
index = targetGroup.editors.length;
648
}
649
// Find the correct EditorInput using the tab info
650
const editorInput = tabInfo?.editorInput;
651
if (!editorInput) {
652
return;
653
}
654
// Move the editor to the target group
655
sourceGroup.moveEditor(editorInput, targetGroup, { index, preserveFocus });
656
return;
657
}
658
659
async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean> {
660
const groups: Map<IEditorGroup, EditorInput[]> = new Map();
661
for (const tabId of tabIds) {
662
const tabInfo = this._tabInfoLookup.get(tabId);
663
const tab = tabInfo?.tab;
664
const group = tabInfo?.group;
665
const editorTab = tabInfo?.editorInput;
666
// If not found skip
667
if (!group || !tab || !tabInfo || !editorTab) {
668
continue;
669
}
670
const groupEditors = groups.get(group);
671
if (!groupEditors) {
672
groups.set(group, [editorTab]);
673
} else {
674
groupEditors.push(editorTab);
675
}
676
}
677
// Loop over keys of the groups map and call closeEditors
678
const results: boolean[] = [];
679
for (const [group, editors] of groups) {
680
results.push(await group.closeEditors(editors, { preserveFocus }));
681
}
682
// TODO @jrieken This isn't quite right how can we say true for some but not others?
683
return results.every(result => result);
684
}
685
686
async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> {
687
const groupCloseResults: boolean[] = [];
688
for (const groupId of groupIds) {
689
const group = this._editorGroupsService.getGroup(groupId);
690
if (group) {
691
groupCloseResults.push(await group.closeAllEditors());
692
// Make sure group is empty but still there before removing it
693
if (group.count === 0 && this._editorGroupsService.getGroup(group.id)) {
694
this._editorGroupsService.removeGroup(group);
695
}
696
}
697
}
698
return groupCloseResults.every(result => result);
699
}
700
//#endregion
701
}
702
703