Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.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 { combinedDisposable, DisposableStore, DisposableMap } from '../../../base/common/lifecycle.js';
8
import { ICodeEditor, isCodeEditor, isDiffEditor, IActiveCodeEditor } from '../../../editor/browser/editorBrowser.js';
9
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
10
import { IEditor } from '../../../editor/common/editorCommon.js';
11
import { ITextModel, shouldSynchronizeModel } from '../../../editor/common/model.js';
12
import { IModelService } from '../../../editor/common/services/model.js';
13
import { ITextModelService } from '../../../editor/common/services/resolverService.js';
14
import { IFileService } from '../../../platform/files/common/files.js';
15
import { extHostCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
16
import { MainThreadDocuments } from './mainThreadDocuments.js';
17
import { MainThreadTextEditor } from './mainThreadEditor.js';
18
import { MainThreadTextEditors } from './mainThreadEditors.js';
19
import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IModelAddedData, ITextEditorAddData, MainContext } from '../common/extHost.protocol.js';
20
import { AbstractTextEditor } from '../../browser/parts/editor/textEditor.js';
21
import { IEditorPane } from '../../common/editor.js';
22
import { EditorGroupColumn, editorGroupToColumn } from '../../services/editor/common/editorGroupColumn.js';
23
import { IEditorService } from '../../services/editor/common/editorService.js';
24
import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';
25
import { ITextFileService } from '../../services/textfile/common/textfiles.js';
26
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
27
import { IWorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js';
28
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
29
import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';
30
import { IPathService } from '../../services/path/common/pathService.js';
31
import { diffSets, diffMaps } from '../../../base/common/collections.js';
32
import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js';
33
import { ViewContainerLocation } from '../../common/views.js';
34
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
35
import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js';
36
37
38
class TextEditorSnapshot {
39
40
readonly id: string;
41
42
constructor(
43
readonly editor: IActiveCodeEditor,
44
) {
45
this.id = `${editor.getId()},${editor.getModel().id}`;
46
}
47
}
48
49
class DocumentAndEditorStateDelta {
50
51
readonly isEmpty: boolean;
52
53
constructor(
54
readonly removedDocuments: ITextModel[],
55
readonly addedDocuments: ITextModel[],
56
readonly removedEditors: TextEditorSnapshot[],
57
readonly addedEditors: TextEditorSnapshot[],
58
readonly oldActiveEditor: string | null | undefined,
59
readonly newActiveEditor: string | null | undefined,
60
) {
61
this.isEmpty = this.removedDocuments.length === 0
62
&& this.addedDocuments.length === 0
63
&& this.removedEditors.length === 0
64
&& this.addedEditors.length === 0
65
&& oldActiveEditor === newActiveEditor;
66
}
67
68
toString(): string {
69
let ret = 'DocumentAndEditorStateDelta\n';
70
ret += `\tRemoved Documents: [${this.removedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;
71
ret += `\tAdded Documents: [${this.addedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;
72
ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`;
73
ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`;
74
ret += `\tNew Active Editor: ${this.newActiveEditor}\n`;
75
return ret;
76
}
77
}
78
79
class DocumentAndEditorState {
80
81
static compute(before: DocumentAndEditorState | undefined, after: DocumentAndEditorState): DocumentAndEditorStateDelta {
82
if (!before) {
83
return new DocumentAndEditorStateDelta(
84
[], [...after.documents.values()],
85
[], [...after.textEditors.values()],
86
undefined, after.activeEditor
87
);
88
}
89
const documentDelta = diffSets(before.documents, after.documents);
90
const editorDelta = diffMaps(before.textEditors, after.textEditors);
91
const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
92
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
93
94
return new DocumentAndEditorStateDelta(
95
documentDelta.removed, documentDelta.added,
96
editorDelta.removed, editorDelta.added,
97
oldActiveEditor, newActiveEditor
98
);
99
}
100
101
constructor(
102
readonly documents: Set<ITextModel>,
103
readonly textEditors: Map<string, TextEditorSnapshot>,
104
readonly activeEditor: string | null | undefined,
105
) {
106
//
107
}
108
}
109
110
const enum ActiveEditorOrder {
111
Editor, Panel
112
}
113
114
class MainThreadDocumentAndEditorStateComputer {
115
116
private readonly _toDispose = new DisposableStore();
117
private readonly _toDisposeOnEditorRemove = new DisposableMap<string>();
118
private _currentState?: DocumentAndEditorState;
119
private _activeEditorOrder: ActiveEditorOrder = ActiveEditorOrder.Editor;
120
121
constructor(
122
private readonly _onDidChangeState: (delta: DocumentAndEditorStateDelta) => void,
123
@IModelService private readonly _modelService: IModelService,
124
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
125
@IEditorService private readonly _editorService: IEditorService,
126
@IPaneCompositePartService private readonly _paneCompositeService: IPaneCompositePartService,
127
) {
128
this._modelService.onModelAdded(this._updateStateOnModelAdd, this, this._toDispose);
129
this._modelService.onModelRemoved(_ => this._updateState(), this, this._toDispose);
130
this._editorService.onDidActiveEditorChange(_ => this._updateState(), this, this._toDispose);
131
132
this._codeEditorService.onCodeEditorAdd(this._onDidAddEditor, this, this._toDispose);
133
this._codeEditorService.onCodeEditorRemove(this._onDidRemoveEditor, this, this._toDispose);
134
this._codeEditorService.listCodeEditors().forEach(this._onDidAddEditor, this);
135
136
Event.filter(this._paneCompositeService.onDidPaneCompositeOpen, event => event.viewContainerLocation === ViewContainerLocation.Panel)(_ => this._activeEditorOrder = ActiveEditorOrder.Panel, undefined, this._toDispose);
137
Event.filter(this._paneCompositeService.onDidPaneCompositeClose, event => event.viewContainerLocation === ViewContainerLocation.Panel)(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);
138
this._editorService.onDidVisibleEditorsChange(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);
139
140
this._updateState();
141
}
142
143
dispose(): void {
144
this._toDispose.dispose();
145
this._toDisposeOnEditorRemove.dispose();
146
}
147
148
private _onDidAddEditor(e: ICodeEditor): void {
149
this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable(
150
e.onDidChangeModel(() => this._updateState()),
151
e.onDidFocusEditorText(() => this._updateState()),
152
e.onDidFocusEditorWidget(() => this._updateState(e))
153
));
154
this._updateState();
155
}
156
157
private _onDidRemoveEditor(e: ICodeEditor): void {
158
const id = e.getId();
159
if (this._toDisposeOnEditorRemove.has(id)) {
160
this._toDisposeOnEditorRemove.deleteAndDispose(id);
161
this._updateState();
162
}
163
}
164
165
private _updateStateOnModelAdd(model: ITextModel): void {
166
if (!shouldSynchronizeModel(model)) {
167
// ignore
168
return;
169
}
170
171
if (!this._currentState) {
172
// too early
173
this._updateState();
174
return;
175
}
176
177
// small (fast) delta
178
this._currentState = new DocumentAndEditorState(
179
this._currentState.documents.add(model),
180
this._currentState.textEditors,
181
this._currentState.activeEditor
182
);
183
184
this._onDidChangeState(new DocumentAndEditorStateDelta(
185
[], [model],
186
[], [],
187
undefined, undefined
188
));
189
}
190
191
private _updateState(widgetFocusCandidate?: ICodeEditor): void {
192
193
// models: ignore too large models
194
const models = new Set<ITextModel>();
195
for (const model of this._modelService.getModels()) {
196
if (shouldSynchronizeModel(model)) {
197
models.add(model);
198
}
199
}
200
201
// editor: only take those that have a not too large model
202
const editors = new Map<string, TextEditorSnapshot>();
203
let activeEditor: string | null = null; // Strict null work. This doesn't like being undefined!
204
205
for (const editor of this._codeEditorService.listCodeEditors()) {
206
if (editor.isSimpleWidget) {
207
continue;
208
}
209
const model = editor.getModel();
210
if (editor.hasModel() && model && shouldSynchronizeModel(model)
211
&& !model.isDisposed() // model disposed
212
&& Boolean(this._modelService.getModel(model.uri)) // model disposing, the flag didn't flip yet but the model service already removed it
213
) {
214
const apiEditor = new TextEditorSnapshot(editor);
215
editors.set(apiEditor.id, apiEditor);
216
if (editor.hasTextFocus() || (widgetFocusCandidate === editor && editor.hasWidgetFocus())) {
217
// text focus has priority, widget focus is tricky because multiple
218
// editors might claim widget focus at the same time. therefore we use a
219
// candidate (which is the editor that has raised an widget focus event)
220
// in addition to the widget focus check
221
activeEditor = apiEditor.id;
222
}
223
}
224
}
225
226
// active editor: if none of the previous editors had focus we try
227
// to match output panels or the active workbench editor with
228
// one of editor we have just computed
229
if (!activeEditor) {
230
let candidate: IEditor | undefined;
231
if (this._activeEditorOrder === ActiveEditorOrder.Editor) {
232
candidate = this._getActiveEditorFromEditorPart() || this._getActiveEditorFromPanel();
233
} else {
234
candidate = this._getActiveEditorFromPanel() || this._getActiveEditorFromEditorPart();
235
}
236
237
if (candidate) {
238
for (const snapshot of editors.values()) {
239
if (candidate === snapshot.editor) {
240
activeEditor = snapshot.id;
241
}
242
}
243
}
244
}
245
246
// compute new state and compare against old
247
const newState = new DocumentAndEditorState(models, editors, activeEditor);
248
const delta = DocumentAndEditorState.compute(this._currentState, newState);
249
if (!delta.isEmpty) {
250
this._currentState = newState;
251
this._onDidChangeState(delta);
252
}
253
}
254
255
private _getActiveEditorFromPanel(): IEditor | undefined {
256
const panel = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel);
257
if (panel instanceof AbstractTextEditor) {
258
const control = panel.getControl();
259
if (isCodeEditor(control)) {
260
return control;
261
}
262
}
263
264
return undefined;
265
}
266
267
private _getActiveEditorFromEditorPart(): IEditor | undefined {
268
let activeTextEditorControl = this._editorService.activeTextEditorControl;
269
if (isDiffEditor(activeTextEditorControl)) {
270
activeTextEditorControl = activeTextEditorControl.getModifiedEditor();
271
}
272
return activeTextEditorControl;
273
}
274
}
275
276
@extHostCustomer
277
export class MainThreadDocumentsAndEditors {
278
279
private readonly _toDispose = new DisposableStore();
280
private readonly _proxy: ExtHostDocumentsAndEditorsShape;
281
private readonly _mainThreadDocuments: MainThreadDocuments;
282
private readonly _mainThreadEditors: MainThreadTextEditors;
283
private readonly _textEditors = new Map<string, MainThreadTextEditor>();
284
285
constructor(
286
extHostContext: IExtHostContext,
287
@IModelService private readonly _modelService: IModelService,
288
@ITextFileService private readonly _textFileService: ITextFileService,
289
@IEditorService private readonly _editorService: IEditorService,
290
@ICodeEditorService codeEditorService: ICodeEditorService,
291
@IFileService fileService: IFileService,
292
@ITextModelService textModelResolverService: ITextModelService,
293
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
294
@IPaneCompositePartService paneCompositeService: IPaneCompositePartService,
295
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
296
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
297
@IUriIdentityService uriIdentityService: IUriIdentityService,
298
@IClipboardService private readonly _clipboardService: IClipboardService,
299
@IPathService pathService: IPathService,
300
@IConfigurationService configurationService: IConfigurationService,
301
@IQuickDiffModelService quickDiffModelService: IQuickDiffModelService
302
) {
303
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors);
304
305
this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService));
306
extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments);
307
308
this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, quickDiffModelService, uriIdentityService));
309
extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors);
310
311
// It is expected that the ctor of the state computer calls our `_onDelta`.
312
this._toDispose.add(new MainThreadDocumentAndEditorStateComputer(delta => this._onDelta(delta), _modelService, codeEditorService, this._editorService, paneCompositeService));
313
}
314
315
dispose(): void {
316
this._toDispose.dispose();
317
}
318
319
private _onDelta(delta: DocumentAndEditorStateDelta): void {
320
321
const removedEditors: string[] = [];
322
const addedEditors: MainThreadTextEditor[] = [];
323
324
// removed models
325
const removedDocuments = delta.removedDocuments.map(m => m.uri);
326
327
// added editors
328
for (const apiEditor of delta.addedEditors) {
329
const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.editor.getModel(),
330
apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._mainThreadDocuments, this._modelService, this._clipboardService);
331
332
this._textEditors.set(apiEditor.id, mainThreadEditor);
333
addedEditors.push(mainThreadEditor);
334
}
335
336
// removed editors
337
for (const { id } of delta.removedEditors) {
338
const mainThreadEditor = this._textEditors.get(id);
339
if (mainThreadEditor) {
340
mainThreadEditor.dispose();
341
this._textEditors.delete(id);
342
removedEditors.push(id);
343
}
344
}
345
346
const extHostDelta: IDocumentsAndEditorsDelta = Object.create(null);
347
let empty = true;
348
if (delta.newActiveEditor !== undefined) {
349
empty = false;
350
extHostDelta.newActiveEditor = delta.newActiveEditor;
351
}
352
if (removedDocuments.length > 0) {
353
empty = false;
354
extHostDelta.removedDocuments = removedDocuments;
355
}
356
if (removedEditors.length > 0) {
357
empty = false;
358
extHostDelta.removedEditors = removedEditors;
359
}
360
if (delta.addedDocuments.length > 0) {
361
empty = false;
362
extHostDelta.addedDocuments = delta.addedDocuments.map(m => this._toModelAddData(m));
363
}
364
if (delta.addedEditors.length > 0) {
365
empty = false;
366
extHostDelta.addedEditors = addedEditors.map(e => this._toTextEditorAddData(e));
367
}
368
369
if (!empty) {
370
// first update ext host
371
this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta);
372
373
// second update dependent document/editor states
374
removedDocuments.forEach(this._mainThreadDocuments.handleModelRemoved, this._mainThreadDocuments);
375
delta.addedDocuments.forEach(this._mainThreadDocuments.handleModelAdded, this._mainThreadDocuments);
376
377
removedEditors.forEach(this._mainThreadEditors.handleTextEditorRemoved, this._mainThreadEditors);
378
addedEditors.forEach(this._mainThreadEditors.handleTextEditorAdded, this._mainThreadEditors);
379
}
380
}
381
382
private _toModelAddData(model: ITextModel): IModelAddedData {
383
return {
384
uri: model.uri,
385
versionId: model.getVersionId(),
386
lines: model.getLinesContent(),
387
EOL: model.getEOL(),
388
languageId: model.getLanguageId(),
389
isDirty: this._textFileService.isDirty(model.uri),
390
encoding: this._textFileService.getEncoding(model.uri)
391
};
392
}
393
394
private _toTextEditorAddData(textEditor: MainThreadTextEditor): ITextEditorAddData {
395
const props = textEditor.getProperties();
396
return {
397
id: textEditor.getId(),
398
documentUri: textEditor.getModel().uri,
399
options: props.options,
400
selections: props.selections,
401
visibleRanges: props.visibleRanges,
402
editorPosition: this._findEditorPosition(textEditor)
403
};
404
}
405
406
private _findEditorPosition(editor: MainThreadTextEditor): EditorGroupColumn | undefined {
407
for (const editorPane of this._editorService.visibleEditorPanes) {
408
if (editor.matches(editorPane)) {
409
return editorGroupToColumn(this._editorGroupService, editorPane.group);
410
}
411
}
412
return undefined;
413
}
414
415
findTextEditorIdFor(editorPane: IEditorPane): string | undefined {
416
for (const [id, editor] of this._textEditors) {
417
if (editor.matches(editorPane)) {
418
return id;
419
}
420
}
421
return undefined;
422
}
423
424
getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined {
425
for (const [id, editor] of this._textEditors) {
426
if (editor.getCodeEditor() === codeEditor) {
427
return id;
428
}
429
}
430
return undefined;
431
}
432
433
getEditor(id: string): MainThreadTextEditor | undefined {
434
return this._textEditors.get(id);
435
}
436
}
437
438