src/vs/workbench/api/common/extHostEditorTabs.ts

444 LOC · 114 covered · 330 uncovered · 25 ranges · 30 concepts · 1 introducers · 26 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/workbench/api/common/extHostDocumentData.ts · 312 LOCcommon/extHostDocumentDa…src/vs/workbench/api/common/extHostDocumentsAndEditors.ts · 197 LOCcommon/extHostDocumentsA…src/vs/workbench/api/common/extHostMcp.ts · 1202 LOCcommon/extHostMcp.tssrc/vs/workbench/api/common/extHostTextEditor.ts · 690 LOCcommon/extHostTextEditor…src/vs/workbench/api/common/extHostVariableResolverService.ts · 176 LOCcommon/extHostVariableRe…src/vs/workbench/services/configurationResolver/common/variableResolver.ts · 345 LOCcommon/variableResolver.…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 3 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 4 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×2 · 4 introduced LOCextHostMcp.ts ×2extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×4 · 6 introduced LOCextHostMcp.ts ×4extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×4 · 13 introduced LOCextHostMcp.ts ×4extHostMcpNode.ts ×8 · 45 introduced LOCextHostMcpNode.ts ×8extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×5 · 25 introduced LOCextHostMcp.ts ×5extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×3 · 23 introduced LOCextHostMcp.ts ×3extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×8 · 60 introduced LOCextHostMcp.ts ×8extHostMcp.ts ×43 · 874 introduced LOCextHostMcp.ts ×43extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1extHostMcp.test|title=Ex…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1extHostMcpNode.test|titl…Focused file · src/vs/workbench/api/common/extHostEditorTabs.ts · 444 LOCcommon/extHostEditorTabs…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- extHostMcp.ts ×43
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 }
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 }
72 > get tabId(): string {
73 return this._dto.id;
74 }
76 > acceptDtoUpdate(dto: IEditorTabDto) {
77 this._dto = dto;
78 this._input = this._initInput();
79 }
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 }
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 this._reconcileTabs(dto);
124 }
126 > get apiObject(): vscode.TabGroup {
127 if (!this._apiObject) {
128 // Don't want to lose reference to parent `this` in the getters
129 const that = this;
130 const obj: vscode.TabGroup = {
131 get isActive() {
132 // We use a getter function here to always ensure at most 1 active group and prevent iteration for being required
133 return that._dto.groupId === that._activeGroupIdGetter();
134 },
135 get viewColumn() {
136 return typeConverters.ViewColumn.to(that._dto.viewColumn);
137 },
138 get activeTab() {
139 return that._tabs.find(tab => tab.tabId === that._activeTabId)?.apiObject;
140 },
141 get tabs() {
142 return Object.freeze(that._tabs.map(tab => tab.apiObject));
143 }
144 };
145 this._apiObject = Object.freeze<vscode.TabGroup>(obj);
146 }
147 return this._apiObject;
148 }
150 > get groupId(): number {
151 return this._dto.groupId;
152 }
154 > get tabs(): ExtHostEditorTab[] {
155 return this._tabs;
156 }
158 > acceptGroupDtoUpdate(dto: IEditorTabGroupDto) {
159 this._dto = dto;
160 }
162 > /**
163 > * Accepts a full group dto during a complete tab-model resync, reusing the
164 > * existing {@link ExtHostEditorTab} instances for tabs that still exist so
165 > * their (and this group's) frozen `apiObject` keeps a stable identity.
166 > * Extensions routinely key `Map`/`WeakMap`/`Set` collections by these
167 > * objects, so recreating them on every resync would break those lookups and
168 > * leak whatever they retain.
169 > */
170 > acceptModelUpdate(dto: IEditorTabGroupDto) {
171 this._dto = dto;
172 this._reconcileTabs(dto);
173 }
175 > private _reconcileTabs(dto: IEditorTabGroupDto) {
176 const existingTabsById = new Map<string, ExtHostEditorTab>();
177 for (const tab of this._tabs) {
178 existingTabsById.set(tab.tabId, tab);
179 }
180
181 this._activeTabId = '';
182 this._tabs = dto.tabs.map(tabDto => {
183 if (tabDto.isActive) {
184 this._activeTabId = tabDto.id;
185 }
186 const existing = existingTabsById.get(tabDto.id);
187 if (existing) {
188 existing.acceptDtoUpdate(tabDto);
189 return existing;
190 }
191 return new ExtHostEditorTab(tabDto, this, () => this.activeTabId());
192 });
193 }
195 > acceptTabOperation(operation: TabOperation): ExtHostEditorTab {
196 // In the open case we add the tab to the group
197 if (operation.kind === TabModelOperationKind.TAB_OPEN) {
198 const tab = new ExtHostEditorTab(operation.tabDto, this, () => this.activeTabId());
199 // Insert tab at editor index
200 this._tabs.splice(operation.index, 0, tab);
201 if (operation.tabDto.isActive) {
202 this._activeTabId = tab.tabId;
203 }
204 return tab;
205 } else if (operation.kind === TabModelOperationKind.TAB_CLOSE) {
206 const tab = this._tabs.splice(operation.index, 1)[0];
207 if (!tab) {
208 throw new Error(`Tab close updated received for index ${operation.index} which does not exist`);
209 }
210 if (tab.tabId === this._activeTabId) {
211 this._activeTabId = '';
212 }
213 return tab;
214 } else if (operation.kind === TabModelOperationKind.TAB_MOVE) {
215 if (operation.oldIndex === undefined) {
216 throw new Error('Invalid old index on move IPC');
217 }
218 // Splice to remove at old index and insert at new index === moving the tab
219 const tab = this._tabs.splice(operation.oldIndex, 1)[0];
220 if (!tab) {
221 throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`);
222 }
223 this._tabs.splice(operation.index, 0, tab);
224 return tab;
225 }
226 const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id);
227 if (!tab) {
228 throw new Error('INVALID tab');
229 }
230 if (operation.tabDto.isActive) {
231 this._activeTabId = operation.tabDto.id;
232 } else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) {
233 // Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id
234 // but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange from
235 // firing incorrectly
236 this._activeTabId = '';
237 }
238 tab.acceptDtoUpdate(operation.tabDto);
239 return tab;
240 }
242 > // Not a getter since it must be a function to be used as a callback for the tabs
243 > activeTabId(): string {
244 return this._activeTabId;
245 }
247 >
248 > export class ExtHostEditorTabs implements IExtHostEditorTabs {
249 > readonly _serviceBrand: undefined;
250 >
251 > private readonly _proxy: MainThreadEditorTabsShape;
252 > private readonly _onDidChangeTabs = new Emitter<vscode.TabChangeEvent>();
253 > private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroupChangeEvent>();
254 >
255 > // Have to use ! because this gets initialized via an RPC proxy
256 > private _activeGroupId!: number;
257 >
258 > private _extHostTabGroups: ExtHostEditorTabGroup[] = [];
259 >
260 > private _apiObject: vscode.TabGroups | undefined;
261 >
262 > constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) {
263 this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs);
264 }
266 > get tabGroups(): vscode.TabGroups {
267 if (!this._apiObject) {
268 const that = this;
269 const obj: vscode.TabGroups = {
270 // never changes -> simple value
271 onDidChangeTabGroups: that._onDidChangeTabGroups.event,
272 onDidChangeTabs: that._onDidChangeTabs.event,
273 // dynamic -> getters
274 get all() {
275 return Object.freeze(that._extHostTabGroups.map(group => group.apiObject));
276 },
277 get activeTabGroup() {
278 const activeTabGroupId = that._activeGroupId;
279 const activeTabGroup = assertReturnsDefined(that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject);
280 return activeTabGroup;
281 },
282 close: async (tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], preserveFocus?: boolean) => {
283 const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup];
284 if (!tabsOrTabGroups.length) {
285 return true;
286 }
287 // Check which type was passed in and call the appropriate close
288 // Casting is needed as typescript doesn't seem to infer enough from this
289 if (isTabGroup(tabsOrTabGroups[0])) {
290 return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus);
291 } else {
292 return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus);
293 }
294 },
295 // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => {
296 // const extHostTab = this._findExtHostTabFromApi(tab);
297 // if (!extHostTab) {
298 // throw new Error('Invalid tab');
299 // }
300 // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus);
301 // return;
302 // }
303 };
304 this._apiObject = Object.freeze(obj);
305 }
306 return this._apiObject;
307 }
309 > $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void {
310
311 const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId));
312 const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId));
313 const diff = diffSets(groupIdsBefore, groupIdsAfter);
314
315 const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject);
316 const opened: vscode.TabGroup[] = [];
317 const changed: vscode.TabGroup[] = [];
318
319 // Reuse the existing group instances for groups that still exist so that
320 // the `vscode.TabGroup` (and nested `vscode.Tab`) objects keep a stable
321 // identity across a full model resync, matching the granular update
322 // paths. Without this, every resync (e.g. opening/closing an editor
323 // group) hands extensions brand-new objects, silently breaking and
324 // leaking any `Map`/`WeakMap`/`Set` keyed by tab groups or tabs.
325 const existingGroupsById = new Map<number, ExtHostEditorTabGroup>();
326 for (const group of this._extHostTabGroups) {
327 existingGroupsById.set(group.groupId, group);
328 }
329
330 this._extHostTabGroups = tabGroups.map(tabGroup => {
331 const existing = existingGroupsById.get(tabGroup.groupId);
332 if (existing) {
333 existing.acceptModelUpdate(tabGroup);
334 changed.push(existing.apiObject);
335 return existing;
336 }
337 const group = new ExtHostEditorTabGroup(tabGroup, () => this._activeGroupId);
338 opened.push(group.apiObject);
339 return group;
340 });
341
342 // Set the active tab group id
343 const activeTabGroupId = assertReturnsDefined(tabGroups.find(group => group.isActive === true)?.groupId);
344 if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) {
345 this._activeGroupId = activeTabGroupId;
346 }
347 this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed }));
348 }
350 > $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) {
351 const group = this._extHostTabGroups.find(group => group.groupId === groupDto.groupId);
352 if (!group) {
353 throw new Error('Update Group IPC call received before group creation.');
354 }
355 group.acceptGroupDtoUpdate(groupDto);
356 if (groupDto.isActive) {
357 this._activeGroupId = groupDto.groupId;
358 }
359 this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] }));
360 }
362 > $acceptTabOperation(operation: TabOperation) {
363 const group = this._extHostTabGroups.find(group => group.groupId === operation.groupId);
364 if (!group) {
365 throw new Error('Update Tabs IPC call received before group creation.');
366 }
367 const tab = group.acceptTabOperation(operation);
368
369 // Construct the tab change event based on the operation
370 switch (operation.kind) {
371 case TabModelOperationKind.TAB_OPEN:
372 this._onDidChangeTabs.fire(Object.freeze({
373 opened: [tab.apiObject],
374 closed: [],
375 changed: []
376 }));
377 return;
378 case TabModelOperationKind.TAB_CLOSE:
379 this._onDidChangeTabs.fire(Object.freeze({
380 opened: [],
381 closed: [tab.apiObject],
382 changed: []
383 }));
384 return;
385 case TabModelOperationKind.TAB_MOVE:
386 case TabModelOperationKind.TAB_UPDATE:
387 this._onDidChangeTabs.fire(Object.freeze({
388 opened: [],
389 closed: [],
390 changed: [tab.apiObject]
391 }));
392 return;
393 }
394 }
396 > private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined {
397 for (const group of this._extHostTabGroups) {
398 for (const tab of group.tabs) {
399 if (tab.apiObject === apiTab) {
400 return tab;
401 }
402 }
403 }
404 return;
405 }
407 > private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined {
408 return this._extHostTabGroups.find(candidate => candidate.apiObject === apiTabGroup);
409 }
411 > private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise<boolean> {
412 const extHostTabIds: string[] = [];
413 for (const tab of tabs) {
414 const extHostTab = this._findExtHostTabFromApi(tab);
415 if (!extHostTab) {
416 throw new Error('Tab close: Invalid tab not found!');
417 }
418 extHostTabIds.push(extHostTab.tabId);
419 }
420 return this._proxy.$closeTab(extHostTabIds, preserveFocus);
421 }
423 > private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise<boolean> {
424 const extHostGroupIds: number[] = [];
425 for (const group of groups) {
426 const extHostGroup = this._findExtHostTabGroupFromApi(group);
427 if (!extHostGroup) {
428 throw new Error('Group close: Invalid group not found!');
429 }
430 extHostGroupIds.push(extHostGroup.groupId);
431 }
432 return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs);
433 }
435 >
436 > //#region Utils
437 function isTabGroup(obj: unknown): obj is vscode.TabGroup {
438 const tabGroup = obj as vscode.TabGroup;
439 if (tabGroup.tabs !== undefined) {
440 return true;
441 }
442 return false;
443 }
444 > //#endregion extHostMcp.ts ×43