Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadEditors.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 { illegalArgument } from '../../../base/common/errors.js';
7
import { IDisposable, dispose, DisposableStore } from '../../../base/common/lifecycle.js';
8
import { equals as objectEquals } from '../../../base/common/objects.js';
9
import { URI, UriComponents } from '../../../base/common/uri.js';
10
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
11
import { IRange } from '../../../editor/common/core/range.js';
12
import { ISelection } from '../../../editor/common/core/selection.js';
13
import { IDecorationOptions, IDecorationRenderOptions } from '../../../editor/common/editorCommon.js';
14
import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js';
15
import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
16
import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution, ITextEditorDiffInformation, isTextEditorDiffInformationEqual, ITextEditorChange } from '../../../platform/editor/common/editor.js';
17
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
18
import { MainThreadTextEditor } from './mainThreadEditor.js';
19
import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from '../common/extHost.protocol.js';
20
import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
21
import { IEditorService } from '../../services/editor/common/editorService.js';
22
import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';
23
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
24
import { IWorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js';
25
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
26
import { IChange } from '../../../editor/common/diff/legacyLinesDiffComputer.js';
27
import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
28
import { IEditorControl } from '../../common/editor.js';
29
import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js';
30
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
31
import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js';
32
import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js';
33
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
34
import { isITextModel } from '../../../editor/common/model.js';
35
import { LineRangeMapping } from '../../../editor/common/diff/rangeMapping.js';
36
import { equals } from '../../../base/common/arrays.js';
37
import { Event } from '../../../base/common/event.js';
38
import { DiffAlgorithmName } from '../../../editor/common/services/editorWorker.js';
39
40
export interface IMainThreadEditorLocator {
41
getEditor(id: string): MainThreadTextEditor | undefined;
42
findTextEditorIdFor(editorControl: IEditorControl): string | undefined;
43
getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined;
44
}
45
46
export class MainThreadTextEditors implements MainThreadTextEditorsShape {
47
48
private static INSTANCE_COUNT: number = 0;
49
50
private readonly _instanceId: string;
51
private readonly _proxy: ExtHostEditorsShape;
52
private readonly _toDispose = new DisposableStore();
53
private _textEditorsListenersMap: { [editorId: string]: IDisposable[] };
54
private _editorPositionData: ITextEditorPositionData | null;
55
private _registeredDecorationTypes: { [decorationType: string]: boolean };
56
57
constructor(
58
private readonly _editorLocator: IMainThreadEditorLocator,
59
extHostContext: IExtHostContext,
60
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
61
@IEditorService private readonly _editorService: IEditorService,
62
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
63
@IConfigurationService private readonly _configurationService: IConfigurationService,
64
@IQuickDiffModelService private readonly _quickDiffModelService: IQuickDiffModelService,
65
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService
66
) {
67
this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT);
68
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors);
69
70
this._textEditorsListenersMap = Object.create(null);
71
this._editorPositionData = null;
72
73
this._toDispose.add(this._editorService.onDidVisibleEditorsChange(() => this._updateActiveAndVisibleTextEditors()));
74
this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors()));
75
this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors()));
76
77
this._registeredDecorationTypes = Object.create(null);
78
}
79
80
dispose(): void {
81
Object.keys(this._textEditorsListenersMap).forEach((editorId) => {
82
dispose(this._textEditorsListenersMap[editorId]);
83
});
84
this._textEditorsListenersMap = Object.create(null);
85
this._toDispose.dispose();
86
for (const decorationType in this._registeredDecorationTypes) {
87
this._codeEditorService.removeDecorationType(decorationType);
88
}
89
this._registeredDecorationTypes = Object.create(null);
90
}
91
92
handleTextEditorAdded(textEditor: MainThreadTextEditor): void {
93
const id = textEditor.getId();
94
const toDispose: IDisposable[] = [];
95
toDispose.push(textEditor.onPropertiesChanged((data) => {
96
this._proxy.$acceptEditorPropertiesChanged(id, data);
97
}));
98
99
const diffInformationObs = this._getTextEditorDiffInformation(textEditor, toDispose);
100
toDispose.push(autorun(reader => {
101
const diffInformation = diffInformationObs.read(reader);
102
this._proxy.$acceptEditorDiffInformation(id, diffInformation);
103
}));
104
105
this._textEditorsListenersMap[id] = toDispose;
106
}
107
108
handleTextEditorRemoved(id: string): void {
109
dispose(this._textEditorsListenersMap[id]);
110
delete this._textEditorsListenersMap[id];
111
}
112
113
private _updateActiveAndVisibleTextEditors(): void {
114
115
// editor columns
116
const editorPositionData = this._getTextEditorPositionData();
117
if (!objectEquals(this._editorPositionData, editorPositionData)) {
118
this._editorPositionData = editorPositionData;
119
this._proxy.$acceptEditorPositionData(this._editorPositionData);
120
}
121
}
122
123
private _getTextEditorPositionData(): ITextEditorPositionData {
124
const result: ITextEditorPositionData = Object.create(null);
125
for (const editorPane of this._editorService.visibleEditorPanes) {
126
const id = this._editorLocator.findTextEditorIdFor(editorPane);
127
if (id) {
128
result[id] = editorGroupToColumn(this._editorGroupService, editorPane.group);
129
}
130
}
131
return result;
132
}
133
134
private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor, toDispose: IDisposable[]): IObservable<ITextEditorDiffInformation[] | undefined> {
135
const codeEditor = textEditor.getCodeEditor();
136
if (!codeEditor) {
137
return constObservable(undefined);
138
}
139
140
// Check if the TextModel belongs to a DiffEditor
141
const [diffEditor] = this._codeEditorService.listDiffEditors()
142
.filter(d =>
143
d.getOriginalEditor().getId() === codeEditor.getId() ||
144
d.getModifiedEditor().getId() === codeEditor.getId());
145
146
const editorModelObs = diffEditor
147
? observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel())
148
: observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel());
149
150
const editorChangesObs = derived<IObservable<{ original: URI; modified: URI; changes: readonly LineRangeMapping[] }[] | undefined>>(reader => {
151
const editorModel = editorModelObs.read(reader);
152
if (!editorModel) {
153
return constObservable(undefined);
154
}
155
156
const editorModelUri = isITextModel(editorModel)
157
? editorModel.uri
158
: editorModel.modified.uri;
159
160
// TextEditor
161
if (isITextModel(editorModel)) {
162
const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri);
163
if (!quickDiffModelRef) {
164
return constObservable(undefined);
165
}
166
167
toDispose.push(quickDiffModelRef);
168
return observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => {
169
return quickDiffModelRef.object.getQuickDiffResults()
170
.map(result => ({
171
original: result.original,
172
modified: result.modified,
173
changes: result.changes2
174
}));
175
});
176
}
177
178
// DirtyDiffModel - we create a dirty diff model for diff editor so that
179
// we can provide multiple "original resources" to diff with the modified
180
// resource.
181
const diffAlgorithm = this._configurationService.getValue<DiffAlgorithmName>('diffEditor.diffAlgorithm');
182
const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri, { algorithm: diffAlgorithm });
183
if (!quickDiffModelRef) {
184
return constObservable(undefined);
185
}
186
187
toDispose.push(quickDiffModelRef);
188
return observableFromEvent(Event.any(quickDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => {
189
const quickDiffInformation = quickDiffModelRef.object.getQuickDiffResults()
190
.map(result => ({
191
original: result.original,
192
modified: result.modified,
193
changes: result.changes2
194
}));
195
196
const diffChanges = diffEditor.getDiffComputationResult()?.changes2 ?? [];
197
const diffInformation = [{
198
original: editorModel.original.uri,
199
modified: editorModel.modified.uri,
200
changes: diffChanges.map(change => change as LineRangeMapping)
201
}];
202
203
return [...quickDiffInformation, ...diffInformation];
204
});
205
});
206
207
return derivedOpts({
208
owner: this,
209
equalsFn: (diff1, diff2) => equals(diff1, diff2, (a, b) => isTextEditorDiffInformationEqual(this._uriIdentityService, a, b))
210
}, reader => {
211
const editorModel = editorModelObs.read(reader);
212
const editorChanges = editorChangesObs.read(reader).read(reader);
213
if (!editorModel || !editorChanges) {
214
return undefined;
215
}
216
217
const documentVersion = isITextModel(editorModel)
218
? editorModel.getVersionId()
219
: editorModel.modified.getVersionId();
220
221
return editorChanges.map(change => {
222
const changes: ITextEditorChange[] = change.changes
223
.map(change => [
224
change.original.startLineNumber,
225
change.original.endLineNumberExclusive,
226
change.modified.startLineNumber,
227
change.modified.endLineNumberExclusive
228
]);
229
230
return {
231
documentVersion,
232
original: change.original,
233
modified: change.modified,
234
changes
235
};
236
});
237
});
238
}
239
240
// --- from extension host process
241
242
async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined> {
243
const uri = URI.revive(resource);
244
245
const editorOptions: ITextEditorOptions = {
246
preserveFocus: options.preserveFocus,
247
pinned: options.pinned,
248
selection: options.selection,
249
// preserve pre 1.38 behaviour to not make group active when preserveFocus: true
250
// but make sure to restore the editor to fix https://github.com/microsoft/vscode/issues/79633
251
activation: options.preserveFocus ? EditorActivation.RESTORE : undefined,
252
override: EditorResolution.EXCLUSIVE_ONLY
253
};
254
255
const input: IResourceEditorInput = {
256
resource: uri,
257
options: editorOptions
258
};
259
260
const editor = await this._editorService.openEditor(input, columnToEditorGroup(this._editorGroupService, this._configurationService, options.position));
261
if (!editor) {
262
return undefined;
263
}
264
// Composite editors are made up of many editors so we return the active one at the time of opening
265
const editorControl = editor.getControl();
266
const codeEditor = getCodeEditor(editorControl);
267
return codeEditor ? this._editorLocator.getIdOfCodeEditor(codeEditor) : undefined;
268
}
269
270
async $tryShowEditor(id: string, position?: EditorGroupColumn): Promise<void> {
271
const mainThreadEditor = this._editorLocator.getEditor(id);
272
if (mainThreadEditor) {
273
const model = mainThreadEditor.getModel();
274
await this._editorService.openEditor({
275
resource: model.uri,
276
options: { preserveFocus: false }
277
}, columnToEditorGroup(this._editorGroupService, this._configurationService, position));
278
return;
279
}
280
}
281
282
async $tryHideEditor(id: string): Promise<void> {
283
const mainThreadEditor = this._editorLocator.getEditor(id);
284
if (mainThreadEditor) {
285
const editorPanes = this._editorService.visibleEditorPanes;
286
for (const editorPane of editorPanes) {
287
if (mainThreadEditor.matches(editorPane)) {
288
await editorPane.group.closeEditor(editorPane.input);
289
return;
290
}
291
}
292
}
293
}
294
295
$trySetSelections(id: string, selections: ISelection[]): Promise<void> {
296
const editor = this._editorLocator.getEditor(id);
297
if (!editor) {
298
return Promise.reject(illegalArgument(`TextEditor(${id})`));
299
}
300
editor.setSelections(selections);
301
return Promise.resolve(undefined);
302
}
303
304
$trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): Promise<void> {
305
key = `${this._instanceId}-${key}`;
306
const editor = this._editorLocator.getEditor(id);
307
if (!editor) {
308
return Promise.reject(illegalArgument(`TextEditor(${id})`));
309
}
310
editor.setDecorations(key, ranges);
311
return Promise.resolve(undefined);
312
}
313
314
$trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise<void> {
315
key = `${this._instanceId}-${key}`;
316
const editor = this._editorLocator.getEditor(id);
317
if (!editor) {
318
return Promise.reject(illegalArgument(`TextEditor(${id})`));
319
}
320
editor.setDecorationsFast(key, ranges);
321
return Promise.resolve(undefined);
322
}
323
324
$tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise<void> {
325
const editor = this._editorLocator.getEditor(id);
326
if (!editor) {
327
return Promise.reject(illegalArgument(`TextEditor(${id})`));
328
}
329
editor.revealRange(range, revealType);
330
return Promise.resolve();
331
}
332
333
$trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Promise<void> {
334
const editor = this._editorLocator.getEditor(id);
335
if (!editor) {
336
return Promise.reject(illegalArgument(`TextEditor(${id})`));
337
}
338
editor.setConfiguration(options);
339
return Promise.resolve(undefined);
340
}
341
342
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise<boolean> {
343
const editor = this._editorLocator.getEditor(id);
344
if (!editor) {
345
return Promise.reject(illegalArgument(`TextEditor(${id})`));
346
}
347
return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts));
348
}
349
350
$tryInsertSnippet(id: string, modelVersionId: number, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise<boolean> {
351
const editor = this._editorLocator.getEditor(id);
352
if (!editor) {
353
return Promise.reject(illegalArgument(`TextEditor(${id})`));
354
}
355
return Promise.resolve(editor.insertSnippet(modelVersionId, template, ranges, opts));
356
}
357
358
$registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: IDecorationRenderOptions): void {
359
key = `${this._instanceId}-${key}`;
360
this._registeredDecorationTypes[key] = true;
361
this._codeEditorService.registerDecorationType(`exthost-api-${extensionId}`, key, options);
362
}
363
364
$removeTextEditorDecorationType(key: string): void {
365
key = `${this._instanceId}-${key}`;
366
delete this._registeredDecorationTypes[key];
367
this._codeEditorService.removeDecorationType(key);
368
}
369
370
$getDiffInformation(id: string): Promise<IChange[]> {
371
const editor = this._editorLocator.getEditor(id);
372
373
if (!editor) {
374
return Promise.reject(new Error('No such TextEditor'));
375
}
376
377
const codeEditor = editor.getCodeEditor();
378
if (!codeEditor) {
379
return Promise.reject(new Error('No such CodeEditor'));
380
}
381
382
const codeEditorId = codeEditor.getId();
383
const diffEditors = this._codeEditorService.listDiffEditors();
384
const [diffEditor] = diffEditors.filter(d => d.getOriginalEditor().getId() === codeEditorId || d.getModifiedEditor().getId() === codeEditorId);
385
386
if (diffEditor) {
387
return Promise.resolve(diffEditor.getLineChanges() || []);
388
}
389
390
if (!codeEditor.hasModel()) {
391
return Promise.resolve([]);
392
}
393
394
const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(codeEditor.getModel().uri);
395
if (!quickDiffModelRef) {
396
return Promise.resolve([]);
397
}
398
399
try {
400
const primaryQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary');
401
const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id);
402
403
return Promise.resolve(primaryQuickDiffChanges.map(change => change.change) ?? []);
404
} finally {
405
quickDiffModelRef.dispose();
406
}
407
}
408
}
409
410
// --- commands
411
412
CommandsRegistry.registerCommand('_workbench.revertAllDirty', async function (accessor: ServicesAccessor) {
413
const environmentService = accessor.get(IEnvironmentService);
414
if (!environmentService.extensionTestsLocationURI) {
415
throw new Error('Command is only available when running extension tests.');
416
}
417
418
const workingCopyService = accessor.get(IWorkingCopyService);
419
for (const workingCopy of workingCopyService.dirtyWorkingCopies) {
420
await workingCopy.revert({ soft: true });
421
}
422
});
423
424