Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/editor.ts
3294 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 { localize } from '../../nls.js';
7
import { EditorResourceAccessor, EditorExtensions, SideBySideEditor, IEditorDescriptor as ICommonEditorDescriptor, EditorCloseContext, IWillInstantiateEditorPaneEvent } from '../common/editor.js';
8
import { EditorInput } from '../common/editor/editorInput.js';
9
import { SyncDescriptor } from '../../platform/instantiation/common/descriptors.js';
10
import { Registry } from '../../platform/registry/common/platform.js';
11
import { EditorPane } from './parts/editor/editorPane.js';
12
import { IConstructorSignature, IInstantiationService, BrandedService, ServicesAccessor } from '../../platform/instantiation/common/instantiation.js';
13
import { IDisposable, toDisposable } from '../../base/common/lifecycle.js';
14
import { Promises } from '../../base/common/async.js';
15
import { IEditorService } from '../services/editor/common/editorService.js';
16
import { IUriIdentityService } from '../../platform/uriIdentity/common/uriIdentity.js';
17
import { IWorkingCopyService } from '../services/workingCopy/common/workingCopyService.js';
18
import { URI } from '../../base/common/uri.js';
19
import { Schemas } from '../../base/common/network.js';
20
import { IEditorGroup } from '../services/editor/common/editorGroupsService.js';
21
import { Iterable } from '../../base/common/iterator.js';
22
import { Emitter } from '../../base/common/event.js';
23
24
//#region Editor Pane Registry
25
26
export interface IEditorPaneDescriptor extends ICommonEditorDescriptor<EditorPane> { }
27
28
export interface IEditorPaneRegistry {
29
30
/**
31
* Registers an editor pane to the platform for the given editor type. The second parameter also supports an
32
* array of input classes to be passed in. If the more than one editor is registered for the same editor
33
* input, the input itself will be asked which editor it prefers if this method is provided. Otherwise
34
* the first editor in the list will be returned.
35
*
36
* @param editorDescriptors A set of constructor functions that return an instance of `EditorInput` for which the
37
* registered editor should be used for.
38
*/
39
registerEditorPane(editorPaneDescriptor: IEditorPaneDescriptor, editorDescriptors: readonly SyncDescriptor<EditorInput>[]): IDisposable;
40
41
/**
42
* Returns the editor pane descriptor for the given editor or `undefined` if none.
43
*/
44
getEditorPane(editor: EditorInput): IEditorPaneDescriptor | undefined;
45
}
46
47
/**
48
* A lightweight descriptor of an editor pane. The descriptor is deferred so that heavy editor
49
* panes can load lazily in the workbench.
50
*/
51
export class EditorPaneDescriptor implements IEditorPaneDescriptor {
52
53
private static readonly instantiatedEditorPanes = new Set<string>();
54
static didInstantiateEditorPane(typeId: string): boolean {
55
return EditorPaneDescriptor.instantiatedEditorPanes.has(typeId);
56
}
57
58
private static readonly _onWillInstantiateEditorPane = new Emitter<IWillInstantiateEditorPaneEvent>();
59
static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event;
60
61
static create<Services extends BrandedService[]>(
62
ctor: { new(group: IEditorGroup, ...services: Services): EditorPane },
63
typeId: string,
64
name: string
65
): EditorPaneDescriptor {
66
return new EditorPaneDescriptor(ctor as IConstructorSignature<EditorPane, [IEditorGroup]>, typeId, name);
67
}
68
69
private constructor(
70
private readonly ctor: IConstructorSignature<EditorPane, [IEditorGroup]>,
71
readonly typeId: string,
72
readonly name: string
73
) { }
74
75
instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane {
76
EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId });
77
78
const pane = instantiationService.createInstance(this.ctor, group);
79
EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId);
80
81
return pane;
82
}
83
84
describes(editorPane: EditorPane): boolean {
85
return editorPane.getId() === this.typeId;
86
}
87
}
88
89
export class EditorPaneRegistry implements IEditorPaneRegistry {
90
91
private readonly mapEditorPanesToEditors = new Map<EditorPaneDescriptor, readonly SyncDescriptor<EditorInput>[]>();
92
93
registerEditorPane(editorPaneDescriptor: EditorPaneDescriptor, editorDescriptors: readonly SyncDescriptor<EditorInput>[]): IDisposable {
94
this.mapEditorPanesToEditors.set(editorPaneDescriptor, editorDescriptors);
95
96
return toDisposable(() => {
97
this.mapEditorPanesToEditors.delete(editorPaneDescriptor);
98
});
99
}
100
101
getEditorPane(editor: EditorInput): EditorPaneDescriptor | undefined {
102
const descriptors = this.findEditorPaneDescriptors(editor);
103
104
if (descriptors.length === 0) {
105
return undefined;
106
}
107
108
if (descriptors.length === 1) {
109
return descriptors[0];
110
}
111
112
return editor.prefersEditorPane(descriptors);
113
}
114
115
private findEditorPaneDescriptors(editor: EditorInput, byInstanceOf?: boolean): EditorPaneDescriptor[] {
116
const matchingEditorPaneDescriptors: EditorPaneDescriptor[] = [];
117
118
for (const editorPane of this.mapEditorPanesToEditors.keys()) {
119
const editorDescriptors = this.mapEditorPanesToEditors.get(editorPane) || [];
120
for (const editorDescriptor of editorDescriptors) {
121
const editorClass = editorDescriptor.ctor;
122
123
// Direct check on constructor type (ignores prototype chain)
124
if (!byInstanceOf && editor.constructor === editorClass) {
125
matchingEditorPaneDescriptors.push(editorPane);
126
break;
127
}
128
129
// Normal instanceof check
130
else if (byInstanceOf && editor instanceof editorClass) {
131
matchingEditorPaneDescriptors.push(editorPane);
132
break;
133
}
134
}
135
}
136
137
// If no descriptors found, continue search using instanceof and prototype chain
138
if (!byInstanceOf && matchingEditorPaneDescriptors.length === 0) {
139
return this.findEditorPaneDescriptors(editor, true);
140
}
141
142
return matchingEditorPaneDescriptors;
143
}
144
145
//#region Used for tests only
146
147
getEditorPaneByType(typeId: string): EditorPaneDescriptor | undefined {
148
return Iterable.find(this.mapEditorPanesToEditors.keys(), editor => editor.typeId === typeId);
149
}
150
151
getEditorPanes(): readonly EditorPaneDescriptor[] {
152
return Array.from(this.mapEditorPanesToEditors.keys());
153
}
154
155
getEditors(): SyncDescriptor<EditorInput>[] {
156
const editorClasses: SyncDescriptor<EditorInput>[] = [];
157
for (const editorPane of this.mapEditorPanesToEditors.keys()) {
158
const editorDescriptors = this.mapEditorPanesToEditors.get(editorPane);
159
if (editorDescriptors) {
160
editorClasses.push(...editorDescriptors.map(editorDescriptor => editorDescriptor.ctor));
161
}
162
}
163
164
return editorClasses;
165
}
166
167
//#endregion
168
}
169
170
Registry.add(EditorExtensions.EditorPane, new EditorPaneRegistry());
171
172
//#endregion
173
174
//#region Editor Close Tracker
175
176
export function whenEditorClosed(accessor: ServicesAccessor, resources: URI[]): Promise<void> {
177
const editorService = accessor.get(IEditorService);
178
const uriIdentityService = accessor.get(IUriIdentityService);
179
const workingCopyService = accessor.get(IWorkingCopyService);
180
181
return new Promise(resolve => {
182
let remainingResources = [...resources];
183
184
// Observe any editor closing from this moment on
185
const listener = editorService.onDidCloseEditor(async event => {
186
if (event.context === EditorCloseContext.MOVE) {
187
return; // ignore move events where the editor will open in another group
188
}
189
190
let primaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.PRIMARY });
191
let secondaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.SECONDARY });
192
193
// Specially handle an editor getting replaced: if the new active editor
194
// matches any of the resources from the closed editor, ignore those
195
// resources because they were actually not closed, but replaced.
196
// (see https://github.com/microsoft/vscode/issues/134299)
197
if (event.context === EditorCloseContext.REPLACE) {
198
const newPrimaryResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
199
const newSecondaryResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.SECONDARY });
200
201
if (uriIdentityService.extUri.isEqual(primaryResource, newPrimaryResource)) {
202
primaryResource = undefined;
203
}
204
205
if (uriIdentityService.extUri.isEqual(secondaryResource, newSecondaryResource)) {
206
secondaryResource = undefined;
207
}
208
}
209
210
// Remove from resources to wait for being closed based on the
211
// resources from editors that got closed
212
remainingResources = remainingResources.filter(resource => {
213
214
// Closing editor matches resource directly: remove from remaining
215
if (uriIdentityService.extUri.isEqual(resource, primaryResource) || uriIdentityService.extUri.isEqual(resource, secondaryResource)) {
216
return false;
217
}
218
219
// Closing editor is untitled with associated resource
220
// that matches resource directly: remove from remaining
221
// but only if the editor was not replaced, otherwise
222
// saving an untitled with associated resource would
223
// release the `--wait` call.
224
// (see https://github.com/microsoft/vscode/issues/141237)
225
if (event.context !== EditorCloseContext.REPLACE) {
226
if (
227
(primaryResource?.scheme === Schemas.untitled && uriIdentityService.extUri.isEqual(resource, primaryResource.with({ scheme: resource.scheme }))) ||
228
(secondaryResource?.scheme === Schemas.untitled && uriIdentityService.extUri.isEqual(resource, secondaryResource.with({ scheme: resource.scheme })))
229
) {
230
return false;
231
}
232
}
233
234
// Editor is not yet closed, so keep it in waiting mode
235
return true;
236
});
237
238
// All resources to wait for being closed are closed
239
if (remainingResources.length === 0) {
240
241
// If auto save is configured with the default delay (1s) it is possible
242
// to close the editor while the save still continues in the background. As such
243
// we have to also check if the editors to track for are dirty and if so wait
244
// for them to get saved.
245
const dirtyResources = resources.filter(resource => workingCopyService.isDirty(resource));
246
if (dirtyResources.length > 0) {
247
await Promises.settled(dirtyResources.map(async resource => await new Promise<void>(resolve => {
248
if (!workingCopyService.isDirty(resource)) {
249
return resolve(); // return early if resource is not dirty
250
}
251
252
// Otherwise resolve promise when resource is saved
253
const listener = workingCopyService.onDidChangeDirty(workingCopy => {
254
if (!workingCopy.isDirty() && uriIdentityService.extUri.isEqual(resource, workingCopy.resource)) {
255
listener.dispose();
256
257
return resolve();
258
}
259
});
260
})));
261
}
262
263
listener.dispose();
264
265
return resolve();
266
}
267
});
268
});
269
}
270
271
//#endregion
272
273
//#region ARIA
274
275
export function computeEditorAriaLabel(input: EditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number | undefined): string {
276
let ariaLabel = input.getAriaLabel();
277
if (group && !group.isPinned(input)) {
278
ariaLabel = localize('preview', "{0}, preview", ariaLabel);
279
}
280
281
if (group?.isSticky(index ?? input)) {
282
ariaLabel = localize('pinned', "{0}, pinned", ariaLabel);
283
}
284
285
// Apply group information to help identify in
286
// which group we are (only if more than one group
287
// is actually opened)
288
if (group && typeof groupCount === 'number' && groupCount > 1) {
289
ariaLabel = `${ariaLabel}, ${group.ariaLabel}`;
290
}
291
292
return ariaLabel;
293
}
294
295
//#endregion
296
297