Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadEditor.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 { Emitter, Event } from '../../../base/common/event.js';
7
import { DisposableStore } from '../../../base/common/lifecycle.js';
8
import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
9
import { RenderLineNumbersType, TextEditorCursorStyle, cursorStyleToString, EditorOption } from '../../../editor/common/config/editorOptions.js';
10
import { IRange, Range } from '../../../editor/common/core/range.js';
11
import { ISelection, Selection } from '../../../editor/common/core/selection.js';
12
import { IDecorationOptions, ScrollType } from '../../../editor/common/editorCommon.js';
13
import { ITextModel, ITextModelUpdateOptions } from '../../../editor/common/model.js';
14
import { ISingleEditOperation } from '../../../editor/common/core/editOperation.js';
15
import { IModelService } from '../../../editor/common/services/model.js';
16
import { SnippetController2 } from '../../../editor/contrib/snippet/browser/snippetController2.js';
17
import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ISnippetOptions, ITextEditorConfigurationUpdate, TextEditorRevealType } from '../common/extHost.protocol.js';
18
import { IEditorPane } from '../../common/editor.js';
19
import { equals } from '../../../base/common/arrays.js';
20
import { CodeEditorStateFlag, EditorState } from '../../../editor/contrib/editorState/browser/editorState.js';
21
import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';
22
import { SnippetParser } from '../../../editor/contrib/snippet/browser/snippetParser.js';
23
import { MainThreadDocuments } from './mainThreadDocuments.js';
24
import { ISnippetEdit } from '../../../editor/contrib/snippet/browser/snippetSession.js';
25
26
export interface IFocusTracker {
27
onGainedFocus(): void;
28
onLostFocus(): void;
29
}
30
31
export class MainThreadTextEditorProperties {
32
33
public static readFromEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): MainThreadTextEditorProperties {
34
const selections = MainThreadTextEditorProperties._readSelectionsFromCodeEditor(previousProperties, codeEditor);
35
const options = MainThreadTextEditorProperties._readOptionsFromCodeEditor(previousProperties, model, codeEditor);
36
const visibleRanges = MainThreadTextEditorProperties._readVisibleRangesFromCodeEditor(previousProperties, codeEditor);
37
return new MainThreadTextEditorProperties(selections, options, visibleRanges);
38
}
39
40
private static _readSelectionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Selection[] {
41
let result: Selection[] | null = null;
42
if (codeEditor) {
43
result = codeEditor.getSelections();
44
}
45
if (!result && previousProperties) {
46
result = previousProperties.selections;
47
}
48
if (!result) {
49
result = [new Selection(1, 1, 1, 1)];
50
}
51
return result;
52
}
53
54
private static _readOptionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): IResolvedTextEditorConfiguration {
55
if (model.isDisposed()) {
56
if (previousProperties) {
57
// shutdown time
58
return previousProperties.options;
59
} else {
60
throw new Error('No valid properties');
61
}
62
}
63
64
let cursorStyle: TextEditorCursorStyle;
65
let lineNumbers: RenderLineNumbersType;
66
if (codeEditor) {
67
const options = codeEditor.getOptions();
68
const lineNumbersOpts = options.get(EditorOption.lineNumbers);
69
cursorStyle = options.get(EditorOption.cursorStyle);
70
lineNumbers = lineNumbersOpts.renderType;
71
} else if (previousProperties) {
72
cursorStyle = previousProperties.options.cursorStyle;
73
lineNumbers = previousProperties.options.lineNumbers;
74
} else {
75
cursorStyle = TextEditorCursorStyle.Line;
76
lineNumbers = RenderLineNumbersType.On;
77
}
78
79
const modelOptions = model.getOptions();
80
return {
81
insertSpaces: modelOptions.insertSpaces,
82
tabSize: modelOptions.tabSize,
83
indentSize: modelOptions.indentSize,
84
originalIndentSize: modelOptions.originalIndentSize,
85
cursorStyle: cursorStyle,
86
lineNumbers: lineNumbers
87
};
88
}
89
90
private static _readVisibleRangesFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Range[] {
91
if (codeEditor) {
92
return codeEditor.getVisibleRanges();
93
}
94
return [];
95
}
96
97
constructor(
98
public readonly selections: Selection[],
99
public readonly options: IResolvedTextEditorConfiguration,
100
public readonly visibleRanges: Range[]
101
) {
102
}
103
104
public generateDelta(oldProps: MainThreadTextEditorProperties | null, selectionChangeSource: string | null): IEditorPropertiesChangeData | null {
105
const delta: IEditorPropertiesChangeData = {
106
options: null,
107
selections: null,
108
visibleRanges: null
109
};
110
111
if (!oldProps || !MainThreadTextEditorProperties._selectionsEqual(oldProps.selections, this.selections)) {
112
delta.selections = {
113
selections: this.selections,
114
source: selectionChangeSource ?? undefined,
115
};
116
}
117
118
if (!oldProps || !MainThreadTextEditorProperties._optionsEqual(oldProps.options, this.options)) {
119
delta.options = this.options;
120
}
121
122
if (!oldProps || !MainThreadTextEditorProperties._rangesEqual(oldProps.visibleRanges, this.visibleRanges)) {
123
delta.visibleRanges = this.visibleRanges;
124
}
125
126
if (delta.selections || delta.options || delta.visibleRanges) {
127
// something changed
128
return delta;
129
}
130
// nothing changed
131
return null;
132
}
133
134
private static _selectionsEqual(a: readonly Selection[], b: readonly Selection[]): boolean {
135
return equals(a, b, (aValue, bValue) => aValue.equalsSelection(bValue));
136
}
137
138
private static _rangesEqual(a: readonly Range[], b: readonly Range[]): boolean {
139
return equals(a, b, (aValue, bValue) => aValue.equalsRange(bValue));
140
}
141
142
private static _optionsEqual(a: IResolvedTextEditorConfiguration, b: IResolvedTextEditorConfiguration): boolean {
143
if (a && !b || !a && b) {
144
return false;
145
}
146
if (!a && !b) {
147
return true;
148
}
149
return (
150
a.tabSize === b.tabSize
151
&& a.indentSize === b.indentSize
152
&& a.insertSpaces === b.insertSpaces
153
&& a.cursorStyle === b.cursorStyle
154
&& a.lineNumbers === b.lineNumbers
155
);
156
}
157
}
158
159
/**
160
* Text Editor that is permanently bound to the same model.
161
* It can be bound or not to a CodeEditor.
162
*/
163
export class MainThreadTextEditor {
164
165
private readonly _id: string;
166
private readonly _model: ITextModel;
167
private readonly _mainThreadDocuments: MainThreadDocuments;
168
private readonly _modelService: IModelService;
169
private readonly _clipboardService: IClipboardService;
170
private readonly _modelListeners = new DisposableStore();
171
private _codeEditor: ICodeEditor | null;
172
private readonly _focusTracker: IFocusTracker;
173
private readonly _codeEditorListeners = new DisposableStore();
174
175
private _properties: MainThreadTextEditorProperties | null;
176
private readonly _onPropertiesChanged: Emitter<IEditorPropertiesChangeData>;
177
178
constructor(
179
id: string,
180
model: ITextModel,
181
codeEditor: ICodeEditor,
182
focusTracker: IFocusTracker,
183
mainThreadDocuments: MainThreadDocuments,
184
modelService: IModelService,
185
clipboardService: IClipboardService,
186
) {
187
this._id = id;
188
this._model = model;
189
this._codeEditor = null;
190
this._properties = null;
191
this._focusTracker = focusTracker;
192
this._mainThreadDocuments = mainThreadDocuments;
193
this._modelService = modelService;
194
this._clipboardService = clipboardService;
195
196
this._onPropertiesChanged = new Emitter<IEditorPropertiesChangeData>();
197
198
this._modelListeners.add(this._model.onDidChangeOptions((e) => {
199
this._updatePropertiesNow(null);
200
}));
201
202
this.setCodeEditor(codeEditor);
203
this._updatePropertiesNow(null);
204
}
205
206
public dispose(): void {
207
this._modelListeners.dispose();
208
this._codeEditor = null;
209
this._codeEditorListeners.dispose();
210
}
211
212
private _updatePropertiesNow(selectionChangeSource: string | null): void {
213
this._setProperties(
214
MainThreadTextEditorProperties.readFromEditor(this._properties, this._model, this._codeEditor),
215
selectionChangeSource
216
);
217
}
218
219
private _setProperties(newProperties: MainThreadTextEditorProperties, selectionChangeSource: string | null): void {
220
const delta = newProperties.generateDelta(this._properties, selectionChangeSource);
221
this._properties = newProperties;
222
if (delta) {
223
this._onPropertiesChanged.fire(delta);
224
}
225
}
226
227
public getId(): string {
228
return this._id;
229
}
230
231
public getModel(): ITextModel {
232
return this._model;
233
}
234
235
public getCodeEditor(): ICodeEditor | null {
236
return this._codeEditor;
237
}
238
239
public hasCodeEditor(codeEditor: ICodeEditor | null): boolean {
240
return (this._codeEditor === codeEditor);
241
}
242
243
public setCodeEditor(codeEditor: ICodeEditor | null): void {
244
if (this.hasCodeEditor(codeEditor)) {
245
// Nothing to do...
246
return;
247
}
248
this._codeEditorListeners.clear();
249
250
this._codeEditor = codeEditor;
251
if (this._codeEditor) {
252
253
// Catch early the case that this code editor gets a different model set and disassociate from this model
254
this._codeEditorListeners.add(this._codeEditor.onDidChangeModel(() => {
255
this.setCodeEditor(null);
256
}));
257
258
this._codeEditorListeners.add(this._codeEditor.onDidFocusEditorWidget(() => {
259
this._focusTracker.onGainedFocus();
260
}));
261
this._codeEditorListeners.add(this._codeEditor.onDidBlurEditorWidget(() => {
262
this._focusTracker.onLostFocus();
263
}));
264
265
let nextSelectionChangeSource: string | null = null;
266
this._codeEditorListeners.add(this._mainThreadDocuments.onIsCaughtUpWithContentChanges((uri) => {
267
if (uri.toString() === this._model.uri.toString()) {
268
const selectionChangeSource = nextSelectionChangeSource;
269
nextSelectionChangeSource = null;
270
this._updatePropertiesNow(selectionChangeSource);
271
}
272
}));
273
274
const isValidCodeEditor = () => {
275
// Due to event timings, it is possible that there is a model change event not yet delivered to us.
276
// > e.g. a model change event is emitted to a listener which then decides to update editor options
277
// > In this case the editor configuration change event reaches us first.
278
// So simply check that the model is still attached to this code editor
279
return (this._codeEditor && this._codeEditor.getModel() === this._model);
280
};
281
282
const updateProperties = (selectionChangeSource: string | null) => {
283
// Some editor events get delivered faster than model content changes. This is
284
// problematic, as this leads to editor properties reaching the extension host
285
// too soon, before the model content change that was the root cause.
286
//
287
// If this case is identified, then let's update editor properties on the next model
288
// content change instead.
289
if (this._mainThreadDocuments.isCaughtUpWithContentChanges(this._model.uri)) {
290
nextSelectionChangeSource = null;
291
this._updatePropertiesNow(selectionChangeSource);
292
} else {
293
// update editor properties on the next model content change
294
nextSelectionChangeSource = selectionChangeSource;
295
}
296
};
297
298
this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => {
299
// selection
300
if (!isValidCodeEditor()) {
301
return;
302
}
303
updateProperties(e.source);
304
}));
305
this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration((e) => {
306
// options
307
if (!isValidCodeEditor()) {
308
return;
309
}
310
updateProperties(null);
311
}));
312
this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => {
313
// visibleRanges
314
if (!isValidCodeEditor()) {
315
return;
316
}
317
updateProperties(null);
318
}));
319
this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => {
320
// visibleRanges
321
if (!isValidCodeEditor()) {
322
return;
323
}
324
updateProperties(null);
325
}));
326
this._updatePropertiesNow(null);
327
}
328
}
329
330
public isVisible(): boolean {
331
return !!this._codeEditor;
332
}
333
334
public getProperties(): MainThreadTextEditorProperties {
335
return this._properties!;
336
}
337
338
public get onPropertiesChanged(): Event<IEditorPropertiesChangeData> {
339
return this._onPropertiesChanged.event;
340
}
341
342
public setSelections(selections: ISelection[]): void {
343
if (this._codeEditor) {
344
this._codeEditor.setSelections(selections);
345
return;
346
}
347
348
const newSelections = selections.map(Selection.liftSelection);
349
this._setProperties(
350
new MainThreadTextEditorProperties(newSelections, this._properties!.options, this._properties!.visibleRanges),
351
null
352
);
353
}
354
355
private _setIndentConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {
356
const creationOpts = this._modelService.getCreationOptions(this._model.getLanguageId(), this._model.uri, this._model.isForSimpleWidget);
357
358
if (newConfiguration.tabSize === 'auto' || newConfiguration.insertSpaces === 'auto') {
359
// one of the options was set to 'auto' => detect indentation
360
let insertSpaces = creationOpts.insertSpaces;
361
let tabSize = creationOpts.tabSize;
362
363
if (newConfiguration.insertSpaces !== 'auto' && typeof newConfiguration.insertSpaces !== 'undefined') {
364
insertSpaces = newConfiguration.insertSpaces;
365
}
366
367
if (newConfiguration.tabSize !== 'auto' && typeof newConfiguration.tabSize !== 'undefined') {
368
tabSize = newConfiguration.tabSize;
369
}
370
371
this._model.detectIndentation(insertSpaces, tabSize);
372
return;
373
}
374
375
const newOpts: ITextModelUpdateOptions = {};
376
if (typeof newConfiguration.insertSpaces !== 'undefined') {
377
newOpts.insertSpaces = newConfiguration.insertSpaces;
378
}
379
if (typeof newConfiguration.tabSize !== 'undefined') {
380
newOpts.tabSize = newConfiguration.tabSize;
381
}
382
if (typeof newConfiguration.indentSize !== 'undefined') {
383
newOpts.indentSize = newConfiguration.indentSize;
384
}
385
this._model.updateOptions(newOpts);
386
}
387
388
public setConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {
389
this._setIndentConfiguration(newConfiguration);
390
391
if (!this._codeEditor) {
392
return;
393
}
394
395
if (newConfiguration.cursorStyle) {
396
const newCursorStyle = cursorStyleToString(newConfiguration.cursorStyle);
397
this._codeEditor.updateOptions({
398
cursorStyle: newCursorStyle
399
});
400
}
401
402
if (typeof newConfiguration.lineNumbers !== 'undefined') {
403
let lineNumbers: 'on' | 'off' | 'relative' | 'interval';
404
switch (newConfiguration.lineNumbers) {
405
case RenderLineNumbersType.On:
406
lineNumbers = 'on';
407
break;
408
case RenderLineNumbersType.Relative:
409
lineNumbers = 'relative';
410
break;
411
case RenderLineNumbersType.Interval:
412
lineNumbers = 'interval';
413
break;
414
default:
415
lineNumbers = 'off';
416
}
417
this._codeEditor.updateOptions({
418
lineNumbers: lineNumbers
419
});
420
}
421
}
422
423
public setDecorations(key: string, ranges: IDecorationOptions[]): void {
424
if (!this._codeEditor) {
425
return;
426
}
427
this._codeEditor.setDecorationsByType('exthost-api', key, ranges);
428
}
429
430
public setDecorationsFast(key: string, _ranges: number[]): void {
431
if (!this._codeEditor) {
432
return;
433
}
434
const ranges: Range[] = [];
435
for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) {
436
ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]);
437
}
438
this._codeEditor.setDecorationsByTypeFast(key, ranges);
439
}
440
441
public revealRange(range: IRange, revealType: TextEditorRevealType): void {
442
if (!this._codeEditor) {
443
return;
444
}
445
switch (revealType) {
446
case TextEditorRevealType.Default:
447
this._codeEditor.revealRange(range, ScrollType.Smooth);
448
break;
449
case TextEditorRevealType.InCenter:
450
this._codeEditor.revealRangeInCenter(range, ScrollType.Smooth);
451
break;
452
case TextEditorRevealType.InCenterIfOutsideViewport:
453
this._codeEditor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);
454
break;
455
case TextEditorRevealType.AtTop:
456
this._codeEditor.revealRangeAtTop(range, ScrollType.Smooth);
457
break;
458
default:
459
console.warn(`Unknown revealType: ${revealType}`);
460
break;
461
}
462
}
463
464
public isFocused(): boolean {
465
if (this._codeEditor) {
466
return this._codeEditor.hasTextFocus();
467
}
468
return false;
469
}
470
471
public matches(editor: IEditorPane): boolean {
472
if (!editor) {
473
return false;
474
}
475
return editor.getControl() === this._codeEditor;
476
}
477
478
public applyEdits(versionIdCheck: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): boolean {
479
if (this._model.getVersionId() !== versionIdCheck) {
480
// throw new Error('Model has changed in the meantime!');
481
// model changed in the meantime
482
return false;
483
}
484
485
if (!this._codeEditor) {
486
// console.warn('applyEdits on invisible editor');
487
return false;
488
}
489
490
if (typeof opts.setEndOfLine !== 'undefined') {
491
this._model.pushEOL(opts.setEndOfLine);
492
}
493
494
const transformedEdits = edits.map((edit): ISingleEditOperation => {
495
return {
496
range: Range.lift(edit.range),
497
text: edit.text,
498
forceMoveMarkers: edit.forceMoveMarkers
499
};
500
});
501
502
if (opts.undoStopBefore) {
503
this._codeEditor.pushUndoStop();
504
}
505
this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits);
506
if (opts.undoStopAfter) {
507
this._codeEditor.pushUndoStop();
508
}
509
return true;
510
}
511
512
async insertSnippet(modelVersionId: number, template: string, ranges: readonly IRange[], opts: ISnippetOptions) {
513
514
if (!this._codeEditor || !this._codeEditor.hasModel()) {
515
return false;
516
}
517
518
// check if clipboard is required and only iff read it (async)
519
let clipboardText: string | undefined;
520
const needsTemplate = SnippetParser.guessNeedsClipboard(template);
521
if (needsTemplate) {
522
const state = new EditorState(this._codeEditor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
523
clipboardText = await this._clipboardService.readText();
524
if (!state.validate(this._codeEditor)) {
525
return false;
526
}
527
}
528
529
if (this._codeEditor.getModel().getVersionId() !== modelVersionId) {
530
return false;
531
}
532
533
const snippetController = SnippetController2.get(this._codeEditor);
534
if (!snippetController) {
535
return false;
536
}
537
538
this._codeEditor.focus();
539
540
// make modifications as snippet edit
541
const edits: ISnippetEdit[] = ranges.map(range => ({ range: Range.lift(range), template }));
542
snippetController.apply(edits, {
543
overwriteBefore: 0, overwriteAfter: 0,
544
undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter,
545
adjustWhitespace: !opts.keepWhitespace,
546
clipboardText
547
});
548
549
return true;
550
}
551
}
552
553