Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/viewModel/viewModelImpl.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 { ArrayQueue } from '../../../base/common/arrays.js';
7
import { RunOnceScheduler } from '../../../base/common/async.js';
8
import { Color } from '../../../base/common/color.js';
9
import { Event } from '../../../base/common/event.js';
10
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
11
import * as platform from '../../../base/common/platform.js';
12
import * as strings from '../../../base/common/strings.js';
13
import { ConfigurationChangedEvent, EditorOption, EDITOR_FONT_DEFAULTS, filterValidationDecorations, filterFontDecorations } from '../config/editorOptions.js';
14
import { CursorsController } from '../cursor/cursor.js';
15
import { CursorConfiguration, CursorState, EditOperationType, IColumnSelectData, PartialCursorState } from '../cursorCommon.js';
16
import { CursorChangeReason } from '../cursorEvents.js';
17
import { IPosition, Position } from '../core/position.js';
18
import { Range } from '../core/range.js';
19
import { ISelection, Selection } from '../core/selection.js';
20
import { ICommand, ICursorState, IViewState, ScrollType } from '../editorCommon.js';
21
import { IEditorConfiguration } from '../config/editorConfiguration.js';
22
import { EndOfLinePreference, IAttachedView, ICursorStateComputer, IGlyphMarginLanesModel, IIdentifiedSingleEditOperation, ITextModel, PositionAffinity, TextDirection, TrackedRangeStickiness } from '../model.js';
23
import { IActiveIndentGuideInfo, BracketGuideOptions, IndentGuide } from '../textModelGuides.js';
24
import { ModelDecorationMinimapOptions, ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from '../model/textModel.js';
25
import * as textModelEvents from '../textModelEvents.js';
26
import { TokenizationRegistry } from '../languages.js';
27
import { ColorId } from '../encodedTokenAttributes.js';
28
import { ILanguageConfigurationService } from '../languages/languageConfigurationRegistry.js';
29
import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js';
30
import { tokenizeLineToHTML } from '../languages/textToHtmlTokenizer.js';
31
import { EditorTheme } from '../editorTheme.js';
32
import * as viewEvents from '../viewEvents.js';
33
import { ViewLayout } from '../viewLayout/viewLayout.js';
34
import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js';
35
import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js';
36
import { ViewEventHandler } from '../viewEventHandler.js';
37
import { ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js';
38
import { ViewModelDecorations } from './viewModelDecorations.js';
39
import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelFontChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelLineHeightChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js';
40
import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js';
41
import { IThemeService } from '../../../platform/theme/common/themeService.js';
42
import { GlyphMarginLanesModel } from './glyphLanesModel.js';
43
import { ICustomLineHeightData } from '../viewLayout/lineHeights.js';
44
import { TextModelEditSource } from '../textModelEditSource.js';
45
import { InlineDecoration } from './inlineDecorations.js';
46
import { ICoordinatesConverter } from '../coordinatesConverter.js';
47
48
const USE_IDENTITY_LINES_COLLECTION = true;
49
50
export class ViewModel extends Disposable implements IViewModel {
51
52
private readonly _editorId: number;
53
private readonly _configuration: IEditorConfiguration;
54
public readonly model: ITextModel;
55
private readonly _eventDispatcher: ViewModelEventDispatcher;
56
public readonly onEvent: Event<OutgoingViewModelEvent>;
57
public cursorConfig: CursorConfiguration;
58
private readonly _updateConfigurationViewLineCount: RunOnceScheduler;
59
private _hasFocus: boolean;
60
private readonly _viewportStart: ViewportStart;
61
private readonly _lines: IViewModelLines;
62
public readonly coordinatesConverter: ICoordinatesConverter;
63
public readonly viewLayout: ViewLayout;
64
private readonly _cursor: CursorsController;
65
private readonly _decorations: ViewModelDecorations;
66
public readonly glyphLanes: IGlyphMarginLanesModel;
67
68
constructor(
69
editorId: number,
70
configuration: IEditorConfiguration,
71
model: ITextModel,
72
domLineBreaksComputerFactory: ILineBreaksComputerFactory,
73
monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory,
74
scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable,
75
private readonly languageConfigurationService: ILanguageConfigurationService,
76
private readonly _themeService: IThemeService,
77
private readonly _attachedView: IAttachedView,
78
private readonly _transactionalTarget: IBatchableTarget,
79
) {
80
super();
81
82
this._editorId = editorId;
83
this._configuration = configuration;
84
this.model = model;
85
this._eventDispatcher = new ViewModelEventDispatcher();
86
this.onEvent = this._eventDispatcher.onEvent;
87
this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService);
88
this._updateConfigurationViewLineCount = this._register(new RunOnceScheduler(() => this._updateConfigurationViewLineCountNow(), 0));
89
this._hasFocus = false;
90
this._viewportStart = ViewportStart.create(this.model);
91
this.glyphLanes = new GlyphMarginLanesModel(0);
92
93
if (USE_IDENTITY_LINES_COLLECTION && this.model.isTooLargeForTokenization()) {
94
95
this._lines = new ViewModelLinesFromModelAsIs(this.model);
96
97
} else {
98
const options = this._configuration.options;
99
const fontInfo = options.get(EditorOption.fontInfo);
100
const wrappingStrategy = options.get(EditorOption.wrappingStrategy);
101
const wrappingInfo = options.get(EditorOption.wrappingInfo);
102
const wrappingIndent = options.get(EditorOption.wrappingIndent);
103
const wordBreak = options.get(EditorOption.wordBreak);
104
const wrapOnEscapedLineFeeds = options.get(EditorOption.wrapOnEscapedLineFeeds);
105
106
this._lines = new ViewModelLinesFromProjectedModel(
107
this._editorId,
108
this.model,
109
domLineBreaksComputerFactory,
110
monospaceLineBreaksComputerFactory,
111
fontInfo,
112
this.model.getOptions().tabSize,
113
wrappingStrategy,
114
wrappingInfo.wrappingColumn,
115
wrappingIndent,
116
wordBreak,
117
wrapOnEscapedLineFeeds
118
);
119
}
120
121
this.coordinatesConverter = this._lines.createCoordinatesConverter();
122
123
this._cursor = this._register(new CursorsController(model, this, this.coordinatesConverter, this.cursorConfig));
124
125
this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), this._getCustomLineHeights(), scheduleAtNextAnimationFrame));
126
127
this._register(this.viewLayout.onDidScroll((e) => {
128
if (e.scrollTopChanged) {
129
this._handleVisibleLinesChanged();
130
}
131
if (e.scrollTopChanged) {
132
this._viewportStart.invalidate();
133
}
134
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e));
135
this._eventDispatcher.emitOutgoingEvent(new ScrollChangedEvent(
136
e.oldScrollWidth, e.oldScrollLeft, e.oldScrollHeight, e.oldScrollTop,
137
e.scrollWidth, e.scrollLeft, e.scrollHeight, e.scrollTop
138
));
139
}));
140
141
this._register(this.viewLayout.onDidContentSizeChange((e) => {
142
this._eventDispatcher.emitOutgoingEvent(e);
143
}));
144
145
this._decorations = new ViewModelDecorations(this._editorId, this.model, this._configuration, this._lines, this.coordinatesConverter);
146
147
this._registerModelEvents();
148
149
this._register(this._configuration.onDidChangeFast((e) => {
150
try {
151
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
152
this._onConfigurationChanged(eventsCollector, e);
153
} finally {
154
this._eventDispatcher.endEmitViewEvents();
155
}
156
}));
157
158
this._register(MinimapTokensColorTracker.getInstance().onDidChange(() => {
159
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent());
160
}));
161
162
this._register(this._themeService.onDidColorThemeChange((theme) => {
163
this._invalidateDecorationsColorCache();
164
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewThemeChangedEvent(theme));
165
}));
166
167
this._updateConfigurationViewLineCountNow();
168
}
169
170
public override dispose(): void {
171
// First remove listeners, as disposing the lines might end up sending
172
// model decoration changed events ... and we no longer care about them ...
173
super.dispose();
174
this._decorations.dispose();
175
this._lines.dispose();
176
this._viewportStart.dispose();
177
this._eventDispatcher.dispose();
178
}
179
180
public createLineBreaksComputer(): ILineBreaksComputer {
181
return this._lines.createLineBreaksComputer();
182
}
183
184
public addViewEventHandler(eventHandler: ViewEventHandler): void {
185
this._eventDispatcher.addViewEventHandler(eventHandler);
186
}
187
188
public removeViewEventHandler(eventHandler: ViewEventHandler): void {
189
this._eventDispatcher.removeViewEventHandler(eventHandler);
190
}
191
192
private _getCustomLineHeights(): ICustomLineHeightData[] {
193
const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights);
194
if (!allowVariableLineHeights) {
195
return [];
196
}
197
const decorations = this.model.getCustomLineHeightsDecorations(this._editorId);
198
return decorations.map((d) => {
199
const lineNumber = d.range.startLineNumber;
200
const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber)));
201
return {
202
decorationId: d.id,
203
startLineNumber: viewRange.startLineNumber,
204
endLineNumber: viewRange.endLineNumber,
205
lineHeight: d.options.lineHeight || 0
206
};
207
});
208
}
209
210
private _updateConfigurationViewLineCountNow(): void {
211
this._configuration.setViewLineCount(this._lines.getViewLineCount());
212
}
213
214
private getModelVisibleRanges(): Range[] {
215
const linesViewportData = this.viewLayout.getLinesViewportData();
216
const viewVisibleRange = new Range(
217
linesViewportData.startLineNumber,
218
this.getLineMinColumn(linesViewportData.startLineNumber),
219
linesViewportData.endLineNumber,
220
this.getLineMaxColumn(linesViewportData.endLineNumber)
221
);
222
const modelVisibleRanges = this._toModelVisibleRanges(viewVisibleRange);
223
return modelVisibleRanges;
224
}
225
226
public visibleLinesStabilized(): void {
227
const modelVisibleRanges = this.getModelVisibleRanges();
228
this._attachedView.setVisibleLines(modelVisibleRanges, true);
229
}
230
231
private _handleVisibleLinesChanged(): void {
232
const modelVisibleRanges = this.getModelVisibleRanges();
233
this._attachedView.setVisibleLines(modelVisibleRanges, false);
234
}
235
236
public setHasFocus(hasFocus: boolean): void {
237
this._hasFocus = hasFocus;
238
this._cursor.setHasFocus(hasFocus);
239
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewFocusChangedEvent(hasFocus));
240
this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus));
241
}
242
243
public setHasWidgetFocus(hasWidgetFocus: boolean): void {
244
this._eventDispatcher.emitOutgoingEvent(new WidgetFocusChangedEvent(!hasWidgetFocus, hasWidgetFocus));
245
}
246
247
public onCompositionStart(): void {
248
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionStartEvent());
249
}
250
251
public onCompositionEnd(): void {
252
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionEndEvent());
253
}
254
255
private _captureStableViewport(): StableViewport {
256
// We might need to restore the current start view range, so save it (if available)
257
// But only if the scroll position is not at the top of the file
258
if (this._viewportStart.isValid && this.viewLayout.getCurrentScrollTop() > 0) {
259
const previousViewportStartViewPosition = new Position(this._viewportStart.viewLineNumber, this.getLineMinColumn(this._viewportStart.viewLineNumber));
260
const previousViewportStartModelPosition = this.coordinatesConverter.convertViewPositionToModelPosition(previousViewportStartViewPosition);
261
return new StableViewport(previousViewportStartModelPosition, this._viewportStart.startLineDelta);
262
}
263
return new StableViewport(null, 0);
264
}
265
266
private _onConfigurationChanged(eventsCollector: ViewModelEventsCollector, e: ConfigurationChangedEvent): void {
267
const stableViewport = this._captureStableViewport();
268
const options = this._configuration.options;
269
const fontInfo = options.get(EditorOption.fontInfo);
270
const wrappingStrategy = options.get(EditorOption.wrappingStrategy);
271
const wrappingInfo = options.get(EditorOption.wrappingInfo);
272
const wrappingIndent = options.get(EditorOption.wrappingIndent);
273
const wordBreak = options.get(EditorOption.wordBreak);
274
275
if (this._lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent, wordBreak)) {
276
eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent());
277
eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent());
278
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
279
this._cursor.onLineMappingChanged(eventsCollector);
280
this._decorations.onLineMappingChanged();
281
this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights());
282
283
this._updateConfigurationViewLineCount.schedule();
284
}
285
286
if (e.hasChanged(EditorOption.readOnly)) {
287
// Must read again all decorations due to readOnly filtering
288
this._decorations.reset();
289
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
290
}
291
292
if (e.hasChanged(EditorOption.renderValidationDecorations)) {
293
this._decorations.reset();
294
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
295
}
296
297
eventsCollector.emitViewEvent(new viewEvents.ViewConfigurationChangedEvent(e));
298
this.viewLayout.onConfigurationChanged(e);
299
300
stableViewport.recoverViewportStart(this.coordinatesConverter, this.viewLayout);
301
302
if (CursorConfiguration.shouldRecreate(e)) {
303
this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService);
304
this._cursor.updateConfiguration(this.cursorConfig);
305
}
306
}
307
308
private _registerModelEvents(): void {
309
310
this._register(this.model.onDidChangeContentOrInjectedText((e) => {
311
try {
312
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
313
314
let hadOtherModelChange = false;
315
let hadModelLineChangeThatChangedLineMapping = false;
316
317
const changes = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.changes : e.changes);
318
const versionId = (e instanceof textModelEvents.InternalModelContentChangeEvent ? e.rawContentChangedEvent.versionId : null);
319
320
// Do a first pass to compute line mappings, and a second pass to actually interpret them
321
const lineBreaksComputer = this._lines.createLineBreaksComputer();
322
for (const change of changes) {
323
switch (change.changeType) {
324
case textModelEvents.RawContentChangedType.LinesInserted: {
325
for (let lineIdx = 0; lineIdx < change.detail.length; lineIdx++) {
326
const line = change.detail[lineIdx];
327
let injectedText = change.injectedTexts[lineIdx];
328
if (injectedText) {
329
injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId));
330
}
331
lineBreaksComputer.addRequest(line, injectedText, null);
332
}
333
break;
334
}
335
case textModelEvents.RawContentChangedType.LineChanged: {
336
let injectedText: textModelEvents.LineInjectedText[] | null = null;
337
if (change.injectedText) {
338
injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId));
339
}
340
lineBreaksComputer.addRequest(change.detail, injectedText, null);
341
break;
342
}
343
}
344
}
345
const lineBreaks = lineBreaksComputer.finalize();
346
const lineBreakQueue = new ArrayQueue(lineBreaks);
347
348
for (const change of changes) {
349
switch (change.changeType) {
350
case textModelEvents.RawContentChangedType.Flush: {
351
this._lines.onModelFlushed();
352
eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent());
353
this._decorations.reset();
354
this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights());
355
hadOtherModelChange = true;
356
break;
357
}
358
case textModelEvents.RawContentChangedType.LinesDeleted: {
359
const linesDeletedEvent = this._lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber);
360
if (linesDeletedEvent !== null) {
361
eventsCollector.emitViewEvent(linesDeletedEvent);
362
this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber);
363
}
364
hadOtherModelChange = true;
365
break;
366
}
367
case textModelEvents.RawContentChangedType.LinesInserted: {
368
const insertedLineBreaks = lineBreakQueue.takeCount(change.detail.length);
369
const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks);
370
if (linesInsertedEvent !== null) {
371
eventsCollector.emitViewEvent(linesInsertedEvent);
372
this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber);
373
}
374
hadOtherModelChange = true;
375
break;
376
}
377
case textModelEvents.RawContentChangedType.LineChanged: {
378
const changedLineBreakData = lineBreakQueue.dequeue()!;
379
const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] =
380
this._lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData);
381
hadModelLineChangeThatChangedLineMapping = lineMappingChanged;
382
if (linesChangedEvent) {
383
eventsCollector.emitViewEvent(linesChangedEvent);
384
}
385
if (linesInsertedEvent) {
386
eventsCollector.emitViewEvent(linesInsertedEvent);
387
this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber);
388
}
389
if (linesDeletedEvent) {
390
eventsCollector.emitViewEvent(linesDeletedEvent);
391
this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber);
392
}
393
break;
394
}
395
case textModelEvents.RawContentChangedType.EOLChanged: {
396
// Nothing to do. The new version will be accepted below
397
break;
398
}
399
}
400
}
401
402
if (versionId !== null) {
403
this._lines.acceptVersionId(versionId);
404
}
405
this.viewLayout.onHeightMaybeChanged();
406
407
if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) {
408
eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent());
409
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
410
this._cursor.onLineMappingChanged(eventsCollector);
411
this._decorations.onLineMappingChanged();
412
}
413
} finally {
414
this._eventDispatcher.endEmitViewEvents();
415
}
416
417
// Update the configuration and reset the centered view line
418
const viewportStartWasValid = this._viewportStart.isValid;
419
this._viewportStart.invalidate();
420
this._configuration.setModelLineCount(this.model.getLineCount());
421
this._updateConfigurationViewLineCountNow();
422
423
// Recover viewport
424
if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && viewportStartWasValid) {
425
const modelRange = this.model._getTrackedRange(this._viewportStart.modelTrackedRange);
426
if (modelRange) {
427
const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition());
428
const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber);
429
this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStart.startLineDelta }, ScrollType.Immediate);
430
}
431
}
432
433
try {
434
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
435
if (e instanceof textModelEvents.InternalModelContentChangeEvent) {
436
eventsCollector.emitOutgoingEvent(new ModelContentChangedEvent(e.contentChangedEvent));
437
}
438
this._cursor.onModelContentChanged(eventsCollector, e);
439
} finally {
440
this._eventDispatcher.endEmitViewEvents();
441
}
442
443
this._handleVisibleLinesChanged();
444
}));
445
446
const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights);
447
if (allowVariableLineHeights) {
448
this._register(this.model.onDidChangeLineHeight((e) => {
449
const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0);
450
451
this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => {
452
for (const change of filteredChanges) {
453
const { decorationId, lineNumber, lineHeight } = change;
454
const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber)));
455
if (lineHeight !== null) {
456
accessor.insertOrChangeCustomLineHeight(decorationId, viewRange.startLineNumber, viewRange.endLineNumber, lineHeight);
457
} else {
458
accessor.removeCustomLineHeight(decorationId);
459
}
460
}
461
});
462
463
// recreate the model event using the filtered changes
464
if (filteredChanges.length > 0) {
465
const filteredEvent = new textModelEvents.ModelLineHeightChangedEvent(filteredChanges);
466
this._eventDispatcher.emitOutgoingEvent(new ModelLineHeightChangedEvent(filteredEvent));
467
}
468
}));
469
}
470
471
const allowVariableFonts = this._configuration.options.get(EditorOption.effectiveAllowVariableFonts);
472
if (allowVariableFonts) {
473
this._register(this.model.onDidChangeFont((e) => {
474
const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0);
475
// recreate the model event using the filtered changes
476
if (filteredChanges.length > 0) {
477
const filteredEvent = new textModelEvents.ModelFontChangedEvent(filteredChanges);
478
this._eventDispatcher.emitOutgoingEvent(new ModelFontChangedEvent(filteredEvent));
479
}
480
}));
481
}
482
483
this._register(this.model.onDidChangeTokens((e) => {
484
const viewRanges: { fromLineNumber: number; toLineNumber: number }[] = [];
485
for (let j = 0, lenJ = e.ranges.length; j < lenJ; j++) {
486
const modelRange = e.ranges[j];
487
const viewStartLineNumber = this.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.fromLineNumber, 1)).lineNumber;
488
const viewEndLineNumber = this.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.toLineNumber, this.model.getLineMaxColumn(modelRange.toLineNumber))).lineNumber;
489
viewRanges[j] = {
490
fromLineNumber: viewStartLineNumber,
491
toLineNumber: viewEndLineNumber
492
};
493
}
494
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges));
495
this._eventDispatcher.emitOutgoingEvent(new ModelTokensChangedEvent(e));
496
}));
497
498
this._register(this.model.onDidChangeLanguageConfiguration((e) => {
499
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent());
500
this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService);
501
this._cursor.updateConfiguration(this.cursorConfig);
502
this._eventDispatcher.emitOutgoingEvent(new ModelLanguageConfigurationChangedEvent(e));
503
}));
504
505
this._register(this.model.onDidChangeLanguage((e) => {
506
this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService);
507
this._cursor.updateConfiguration(this.cursorConfig);
508
this._eventDispatcher.emitOutgoingEvent(new ModelLanguageChangedEvent(e));
509
}));
510
511
this._register(this.model.onDidChangeOptions((e) => {
512
// A tab size change causes a line mapping changed event => all view parts will repaint OK, no further event needed here
513
if (this._lines.setTabSize(this.model.getOptions().tabSize)) {
514
try {
515
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
516
eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent());
517
eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent());
518
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
519
this._cursor.onLineMappingChanged(eventsCollector);
520
this._decorations.onLineMappingChanged();
521
this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights());
522
} finally {
523
this._eventDispatcher.endEmitViewEvents();
524
}
525
this._updateConfigurationViewLineCount.schedule();
526
}
527
528
this.cursorConfig = new CursorConfiguration(this.model.getLanguageId(), this.model.getOptions(), this._configuration, this.languageConfigurationService);
529
this._cursor.updateConfiguration(this.cursorConfig);
530
531
this._eventDispatcher.emitOutgoingEvent(new ModelOptionsChangedEvent(e));
532
}));
533
534
this._register(this.model.onDidChangeDecorations((e) => {
535
this._decorations.onModelDecorationsChanged();
536
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e));
537
this._eventDispatcher.emitOutgoingEvent(new ModelDecorationsChangedEvent(e));
538
}));
539
}
540
541
private readonly hiddenAreasModel = new HiddenAreasModel();
542
private previousHiddenAreas: readonly Range[] = [];
543
544
public getFontSizeAtPosition(position: IPosition): string | null {
545
const allowVariableFonts = this._configuration.options.get(EditorOption.effectiveAllowVariableFonts);
546
if (!allowVariableFonts) {
547
return null;
548
}
549
const fontDecorations = this.model.getFontDecorationsInRange(Range.fromPositions(position), this._editorId);
550
let fontSize: string = this._configuration.options.get(EditorOption.fontInfo).fontSize + 'px';
551
for (const fontDecoration of fontDecorations) {
552
if (fontDecoration.options.fontSize) {
553
fontSize = fontDecoration.options.fontSize;
554
break;
555
}
556
}
557
return fontSize;
558
}
559
560
/**
561
* @param forceUpdate If true, the hidden areas will be updated even if the new ranges are the same as the previous ranges.
562
* This is because the model might have changed, which resets the hidden areas, but not the last cached value.
563
* This needs a better fix in the future.
564
*/
565
public setHiddenAreas(ranges: Range[], source?: unknown, forceUpdate?: boolean): void {
566
this.hiddenAreasModel.setHiddenAreas(source, ranges);
567
const mergedRanges = this.hiddenAreasModel.getMergedRanges();
568
if (mergedRanges === this.previousHiddenAreas && !forceUpdate) {
569
return;
570
}
571
572
this.previousHiddenAreas = mergedRanges;
573
574
const stableViewport = this._captureStableViewport();
575
576
let lineMappingChanged = false;
577
try {
578
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
579
lineMappingChanged = this._lines.setHiddenAreas(mergedRanges);
580
if (lineMappingChanged) {
581
eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent());
582
eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent());
583
eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null));
584
this._cursor.onLineMappingChanged(eventsCollector);
585
this._decorations.onLineMappingChanged();
586
this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights());
587
this.viewLayout.onHeightMaybeChanged();
588
}
589
590
const firstModelLineInViewPort = stableViewport.viewportStartModelPosition?.lineNumber;
591
const firstModelLineIsHidden = firstModelLineInViewPort && mergedRanges.some(range => range.startLineNumber <= firstModelLineInViewPort && firstModelLineInViewPort <= range.endLineNumber);
592
if (!firstModelLineIsHidden) {
593
stableViewport.recoverViewportStart(this.coordinatesConverter, this.viewLayout);
594
}
595
} finally {
596
this._eventDispatcher.endEmitViewEvents();
597
}
598
this._updateConfigurationViewLineCount.schedule();
599
600
if (lineMappingChanged) {
601
this._eventDispatcher.emitOutgoingEvent(new HiddenAreasChangedEvent());
602
}
603
}
604
605
public getVisibleRangesPlusViewportAboveBelow(): Range[] {
606
const layoutInfo = this._configuration.options.get(EditorOption.layoutInfo);
607
const lineHeight = this._configuration.options.get(EditorOption.lineHeight);
608
const linesAround = Math.max(20, Math.round(layoutInfo.height / lineHeight));
609
const partialData = this.viewLayout.getLinesViewportData();
610
const startViewLineNumber = Math.max(1, partialData.completelyVisibleStartLineNumber - linesAround);
611
const endViewLineNumber = Math.min(this.getLineCount(), partialData.completelyVisibleEndLineNumber + linesAround);
612
613
return this._toModelVisibleRanges(new Range(
614
startViewLineNumber, this.getLineMinColumn(startViewLineNumber),
615
endViewLineNumber, this.getLineMaxColumn(endViewLineNumber)
616
));
617
}
618
619
public getVisibleRanges(): Range[] {
620
const visibleViewRange = this.getCompletelyVisibleViewRange();
621
return this._toModelVisibleRanges(visibleViewRange);
622
}
623
624
public getHiddenAreas(): Range[] {
625
return this._lines.getHiddenAreas();
626
}
627
628
private _toModelVisibleRanges(visibleViewRange: Range): Range[] {
629
const visibleRange = this.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange);
630
const hiddenAreas = this._lines.getHiddenAreas();
631
632
if (hiddenAreas.length === 0) {
633
return [visibleRange];
634
}
635
636
const result: Range[] = [];
637
let resultLen = 0;
638
let startLineNumber = visibleRange.startLineNumber;
639
let startColumn = visibleRange.startColumn;
640
const endLineNumber = visibleRange.endLineNumber;
641
const endColumn = visibleRange.endColumn;
642
for (let i = 0, len = hiddenAreas.length; i < len; i++) {
643
const hiddenStartLineNumber = hiddenAreas[i].startLineNumber;
644
const hiddenEndLineNumber = hiddenAreas[i].endLineNumber;
645
646
if (hiddenEndLineNumber < startLineNumber) {
647
continue;
648
}
649
if (hiddenStartLineNumber > endLineNumber) {
650
continue;
651
}
652
653
if (startLineNumber < hiddenStartLineNumber) {
654
result[resultLen++] = new Range(
655
startLineNumber, startColumn,
656
hiddenStartLineNumber - 1, this.model.getLineMaxColumn(hiddenStartLineNumber - 1)
657
);
658
}
659
startLineNumber = hiddenEndLineNumber + 1;
660
startColumn = 1;
661
}
662
663
if (startLineNumber < endLineNumber || (startLineNumber === endLineNumber && startColumn < endColumn)) {
664
result[resultLen++] = new Range(
665
startLineNumber, startColumn,
666
endLineNumber, endColumn
667
);
668
}
669
670
return result;
671
}
672
673
public getCompletelyVisibleViewRange(): Range {
674
const partialData = this.viewLayout.getLinesViewportData();
675
const startViewLineNumber = partialData.completelyVisibleStartLineNumber;
676
const endViewLineNumber = partialData.completelyVisibleEndLineNumber;
677
678
return new Range(
679
startViewLineNumber, this.getLineMinColumn(startViewLineNumber),
680
endViewLineNumber, this.getLineMaxColumn(endViewLineNumber)
681
);
682
}
683
684
public getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range {
685
const partialData = this.viewLayout.getLinesViewportDataAtScrollTop(scrollTop);
686
const startViewLineNumber = partialData.completelyVisibleStartLineNumber;
687
const endViewLineNumber = partialData.completelyVisibleEndLineNumber;
688
689
return new Range(
690
startViewLineNumber, this.getLineMinColumn(startViewLineNumber),
691
endViewLineNumber, this.getLineMaxColumn(endViewLineNumber)
692
);
693
}
694
695
public saveState(): IViewState {
696
const compatViewState = this.viewLayout.saveState();
697
698
const scrollTop = compatViewState.scrollTop;
699
const firstViewLineNumber = this.viewLayout.getLineNumberAtVerticalOffset(scrollTop);
700
const firstPosition = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(firstViewLineNumber, this.getLineMinColumn(firstViewLineNumber)));
701
const firstPositionDeltaTop = this.viewLayout.getVerticalOffsetForLineNumber(firstViewLineNumber) - scrollTop;
702
703
return {
704
scrollLeft: compatViewState.scrollLeft,
705
firstPosition: firstPosition,
706
firstPositionDeltaTop: firstPositionDeltaTop
707
};
708
}
709
710
public reduceRestoreState(state: IViewState): { scrollLeft: number; scrollTop: number } {
711
if (typeof state.firstPosition === 'undefined') {
712
// This is a view state serialized by an older version
713
return this._reduceRestoreStateCompatibility(state);
714
}
715
716
const modelPosition = this.model.validatePosition(state.firstPosition);
717
const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
718
const scrollTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber) - state.firstPositionDeltaTop;
719
return {
720
scrollLeft: state.scrollLeft,
721
scrollTop: scrollTop
722
};
723
}
724
725
private _reduceRestoreStateCompatibility(state: IViewState): { scrollLeft: number; scrollTop: number } {
726
return {
727
scrollLeft: state.scrollLeft,
728
scrollTop: state.scrollTopWithoutViewZones!
729
};
730
}
731
732
private getTabSize(): number {
733
return this.model.getOptions().tabSize;
734
}
735
736
public getLineCount(): number {
737
return this._lines.getViewLineCount();
738
}
739
740
/**
741
* Gives a hint that a lot of requests are about to come in for these line numbers.
742
*/
743
public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void {
744
this._viewportStart.update(this, startLineNumber);
745
}
746
747
public getActiveIndentGuide(lineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo {
748
return this._lines.getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber);
749
}
750
751
public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] {
752
return this._lines.getViewLinesIndentGuides(startLineNumber, endLineNumber);
753
}
754
755
public getBracketGuidesInRangeByLine(startLineNumber: number, endLineNumber: number, activePosition: IPosition | null, options: BracketGuideOptions): IndentGuide[][] {
756
return this._lines.getViewLinesBracketGuides(startLineNumber, endLineNumber, activePosition, options);
757
}
758
759
public getLineContent(lineNumber: number): string {
760
return this._lines.getViewLineContent(lineNumber);
761
}
762
763
public getLineLength(lineNumber: number): number {
764
return this._lines.getViewLineLength(lineNumber);
765
}
766
767
public getLineMinColumn(lineNumber: number): number {
768
return this._lines.getViewLineMinColumn(lineNumber);
769
}
770
771
public getLineMaxColumn(lineNumber: number): number {
772
return this._lines.getViewLineMaxColumn(lineNumber);
773
}
774
775
public getLineFirstNonWhitespaceColumn(lineNumber: number): number {
776
const result = strings.firstNonWhitespaceIndex(this.getLineContent(lineNumber));
777
if (result === -1) {
778
return 0;
779
}
780
return result + 1;
781
}
782
783
public getLineLastNonWhitespaceColumn(lineNumber: number): number {
784
const result = strings.lastNonWhitespaceIndex(this.getLineContent(lineNumber));
785
if (result === -1) {
786
return 0;
787
}
788
return result + 2;
789
}
790
791
public getMinimapDecorationsInRange(range: Range): ViewModelDecoration[] {
792
return this._decorations.getMinimapDecorationsInRange(range);
793
}
794
795
public getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[] {
796
return this._decorations.getDecorationsViewportData(visibleRange).decorations;
797
}
798
799
public getInjectedTextAt(viewPosition: Position): InjectedText | null {
800
return this._lines.getInjectedTextAt(viewPosition);
801
}
802
803
private _getTextDirection(lineNumber: number, decorations: ViewModelDecoration[]): TextDirection {
804
let rtlCount = 0;
805
806
for (const decoration of decorations) {
807
const range = decoration.range;
808
if (range.startLineNumber > lineNumber || range.endLineNumber < lineNumber) {
809
continue;
810
}
811
const textDirection = decoration.options.textDirection;
812
if (textDirection === TextDirection.RTL) {
813
rtlCount++;
814
} else if (textDirection === TextDirection.LTR) {
815
rtlCount--;
816
}
817
}
818
819
return rtlCount > 0 ? TextDirection.RTL : TextDirection.LTR;
820
}
821
822
public getTextDirection(lineNumber: number): TextDirection {
823
const decorationsCollection = this._decorations.getDecorationsOnLine(lineNumber);
824
return this._getTextDirection(lineNumber, decorationsCollection.decorations);
825
}
826
827
public getViewportViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData {
828
const viewportDecorationsCollection = this._decorations.getDecorationsViewportData(visibleRange);
829
const inlineDecorations = viewportDecorationsCollection.inlineDecorations[lineNumber - visibleRange.startLineNumber];
830
return this._getViewLineRenderingData(lineNumber, inlineDecorations, viewportDecorationsCollection.hasVariableFonts, viewportDecorationsCollection.decorations);
831
}
832
833
public getViewLineRenderingData(lineNumber: number): ViewLineRenderingData {
834
const decorationsCollection = this._decorations.getDecorationsOnLine(lineNumber);
835
return this._getViewLineRenderingData(lineNumber, decorationsCollection.inlineDecorations[0], decorationsCollection.hasVariableFonts, decorationsCollection.decorations);
836
}
837
838
private _getViewLineRenderingData(lineNumber: number, inlineDecorations: InlineDecoration[], hasVariableFonts: boolean, decorations: ViewModelDecoration[]): ViewLineRenderingData {
839
const mightContainRTL = this.model.mightContainRTL();
840
const mightContainNonBasicASCII = this.model.mightContainNonBasicASCII();
841
const tabSize = this.getTabSize();
842
const lineData = this._lines.getViewLineData(lineNumber);
843
844
if (lineData.inlineDecorations) {
845
inlineDecorations = [
846
...inlineDecorations,
847
...lineData.inlineDecorations.map(d =>
848
d.toInlineDecoration(lineNumber)
849
)
850
];
851
}
852
853
return new ViewLineRenderingData(
854
lineData.minColumn,
855
lineData.maxColumn,
856
lineData.content,
857
lineData.continuesWithWrappedLine,
858
mightContainRTL,
859
mightContainNonBasicASCII,
860
lineData.tokens,
861
inlineDecorations,
862
tabSize,
863
lineData.startVisibleColumn,
864
this._getTextDirection(lineNumber, decorations),
865
hasVariableFonts
866
);
867
}
868
869
public getViewLineData(lineNumber: number): ViewLineData {
870
return this._lines.getViewLineData(lineNumber);
871
}
872
873
public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): MinimapLinesRenderingData {
874
const result = this._lines.getViewLinesData(startLineNumber, endLineNumber, needed);
875
return new MinimapLinesRenderingData(
876
this.getTabSize(),
877
result
878
);
879
}
880
881
public getAllOverviewRulerDecorations(theme: EditorTheme): OverviewRulerDecorationsGroup[] {
882
const decorations = this.model.getOverviewRulerDecorations(this._editorId, filterValidationDecorations(this._configuration.options), filterFontDecorations(this._configuration.options));
883
const result = new OverviewRulerDecorations();
884
for (const decoration of decorations) {
885
const decorationOptions = <ModelDecorationOptions>decoration.options;
886
const opts = decorationOptions.overviewRuler;
887
if (!opts) {
888
continue;
889
}
890
const lane = <number>opts.position;
891
if (lane === 0) {
892
continue;
893
}
894
const color = opts.getColor(theme.value);
895
const viewStartLineNumber = this.coordinatesConverter.getViewLineNumberOfModelPosition(decoration.range.startLineNumber, decoration.range.startColumn);
896
const viewEndLineNumber = this.coordinatesConverter.getViewLineNumberOfModelPosition(decoration.range.endLineNumber, decoration.range.endColumn);
897
898
result.accept(color, decorationOptions.zIndex, viewStartLineNumber, viewEndLineNumber, lane);
899
}
900
return result.asArray;
901
}
902
903
private _invalidateDecorationsColorCache(): void {
904
const decorations = this.model.getOverviewRulerDecorations();
905
for (const decoration of decorations) {
906
const opts1 = <ModelDecorationOverviewRulerOptions>decoration.options.overviewRuler;
907
opts1?.invalidateCachedColor();
908
const opts2 = <ModelDecorationMinimapOptions>decoration.options.minimap;
909
opts2?.invalidateCachedColor();
910
}
911
}
912
913
public getValueInRange(range: Range, eol: EndOfLinePreference): string {
914
const modelRange = this.coordinatesConverter.convertViewRangeToModelRange(range);
915
return this.model.getValueInRange(modelRange, eol);
916
}
917
918
public getValueLengthInRange(range: Range, eol: EndOfLinePreference): number {
919
const modelRange = this.coordinatesConverter.convertViewRangeToModelRange(range);
920
return this.model.getValueLengthInRange(modelRange, eol);
921
}
922
923
public modifyPosition(position: Position, offset: number): Position {
924
const modelPosition = this.coordinatesConverter.convertViewPositionToModelPosition(position);
925
const resultModelPosition = this.model.modifyPosition(modelPosition, offset);
926
return this.coordinatesConverter.convertModelPositionToViewPosition(resultModelPosition);
927
}
928
929
public deduceModelPositionRelativeToViewPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position {
930
const modelAnchor = this.coordinatesConverter.convertViewPositionToModelPosition(viewAnchorPosition);
931
if (this.model.getEOL().length === 2) {
932
// This model uses CRLF, so the delta must take that into account
933
if (deltaOffset < 0) {
934
deltaOffset -= lineFeedCnt;
935
} else {
936
deltaOffset += lineFeedCnt;
937
}
938
}
939
940
const modelAnchorOffset = this.model.getOffsetAt(modelAnchor);
941
const resultOffset = modelAnchorOffset + deltaOffset;
942
return this.model.getPositionAt(resultOffset);
943
}
944
945
public getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): string | string[] {
946
const newLineCharacter = forceCRLF ? '\r\n' : this.model.getEOL();
947
948
modelRanges = modelRanges.slice(0);
949
modelRanges.sort(Range.compareRangesUsingStarts);
950
951
let hasEmptyRange = false;
952
let hasNonEmptyRange = false;
953
for (const range of modelRanges) {
954
if (range.isEmpty()) {
955
hasEmptyRange = true;
956
} else {
957
hasNonEmptyRange = true;
958
}
959
}
960
961
if (!hasNonEmptyRange) {
962
// all ranges are empty
963
if (!emptySelectionClipboard) {
964
return '';
965
}
966
967
const modelLineNumbers = modelRanges.map((r) => r.startLineNumber);
968
969
let result = '';
970
for (let i = 0; i < modelLineNumbers.length; i++) {
971
if (i > 0 && modelLineNumbers[i - 1] === modelLineNumbers[i]) {
972
continue;
973
}
974
result += this.model.getLineContent(modelLineNumbers[i]) + newLineCharacter;
975
}
976
return result;
977
}
978
979
if (hasEmptyRange && emptySelectionClipboard) {
980
// mixed empty selections and non-empty selections
981
const result: string[] = [];
982
let prevModelLineNumber = 0;
983
for (const modelRange of modelRanges) {
984
const modelLineNumber = modelRange.startLineNumber;
985
if (modelRange.isEmpty()) {
986
if (modelLineNumber !== prevModelLineNumber) {
987
result.push(this.model.getLineContent(modelLineNumber));
988
}
989
} else {
990
result.push(this.model.getValueInRange(modelRange, forceCRLF ? EndOfLinePreference.CRLF : EndOfLinePreference.TextDefined));
991
}
992
prevModelLineNumber = modelLineNumber;
993
}
994
return result.length === 1 ? result[0] : result;
995
}
996
997
const result: string[] = [];
998
for (const modelRange of modelRanges) {
999
if (!modelRange.isEmpty()) {
1000
result.push(this.model.getValueInRange(modelRange, forceCRLF ? EndOfLinePreference.CRLF : EndOfLinePreference.TextDefined));
1001
}
1002
}
1003
return result.length === 1 ? result[0] : result;
1004
}
1005
1006
public getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string; mode: string } | null {
1007
const languageId = this.model.getLanguageId();
1008
if (languageId === PLAINTEXT_LANGUAGE_ID) {
1009
return null;
1010
}
1011
1012
if (modelRanges.length !== 1) {
1013
// no multiple selection support at this time
1014
return null;
1015
}
1016
1017
let range = modelRanges[0];
1018
if (range.isEmpty()) {
1019
if (!emptySelectionClipboard) {
1020
// nothing to copy
1021
return null;
1022
}
1023
const lineNumber = range.startLineNumber;
1024
range = new Range(lineNumber, this.model.getLineMinColumn(lineNumber), lineNumber, this.model.getLineMaxColumn(lineNumber));
1025
}
1026
1027
const fontInfo = this._configuration.options.get(EditorOption.fontInfo);
1028
const colorMap = this._getColorMap();
1029
const hasBadChars = (/[:;\\\/<>]/.test(fontInfo.fontFamily));
1030
const useDefaultFontFamily = (hasBadChars || fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily);
1031
let fontFamily: string;
1032
if (useDefaultFontFamily) {
1033
fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
1034
} else {
1035
fontFamily = fontInfo.fontFamily;
1036
fontFamily = fontFamily.replace(/"/g, '\'');
1037
const hasQuotesOrIsList = /[,']/.test(fontFamily);
1038
if (!hasQuotesOrIsList) {
1039
const needsQuotes = /[+ ]/.test(fontFamily);
1040
if (needsQuotes) {
1041
fontFamily = `'${fontFamily}'`;
1042
}
1043
}
1044
fontFamily = `${fontFamily}, ${EDITOR_FONT_DEFAULTS.fontFamily}`;
1045
}
1046
1047
return {
1048
mode: languageId,
1049
html: (
1050
`<div style="`
1051
+ `color: ${colorMap[ColorId.DefaultForeground]};`
1052
+ `background-color: ${colorMap[ColorId.DefaultBackground]};`
1053
+ `font-family: ${fontFamily};`
1054
+ `font-weight: ${fontInfo.fontWeight};`
1055
+ `font-size: ${fontInfo.fontSize}px;`
1056
+ `line-height: ${fontInfo.lineHeight}px;`
1057
+ `white-space: pre;`
1058
+ `">`
1059
+ this._getHTMLToCopy(range, colorMap)
1060
+ '</div>'
1061
)
1062
};
1063
}
1064
1065
private _getHTMLToCopy(modelRange: Range, colorMap: string[]): string {
1066
const startLineNumber = modelRange.startLineNumber;
1067
const startColumn = modelRange.startColumn;
1068
const endLineNumber = modelRange.endLineNumber;
1069
const endColumn = modelRange.endColumn;
1070
1071
const tabSize = this.getTabSize();
1072
1073
let result = '';
1074
1075
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
1076
const lineTokens = this.model.tokenization.getLineTokens(lineNumber);
1077
const lineContent = lineTokens.getLineContent();
1078
const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0);
1079
const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length);
1080
1081
if (lineContent === '') {
1082
result += '<br>';
1083
} else {
1084
result += tokenizeLineToHTML(lineContent, lineTokens.inflate(), colorMap, startOffset, endOffset, tabSize, platform.isWindows);
1085
}
1086
}
1087
1088
return result;
1089
}
1090
1091
private _getColorMap(): string[] {
1092
const colorMap = TokenizationRegistry.getColorMap();
1093
const result: string[] = ['#000000'];
1094
if (colorMap) {
1095
for (let i = 1, len = colorMap.length; i < len; i++) {
1096
result[i] = Color.Format.CSS.formatHex(colorMap[i]);
1097
}
1098
}
1099
return result;
1100
}
1101
1102
//#region cursor operations
1103
1104
public getPrimaryCursorState(): CursorState {
1105
return this._cursor.getPrimaryCursorState();
1106
}
1107
public getLastAddedCursorIndex(): number {
1108
return this._cursor.getLastAddedCursorIndex();
1109
}
1110
public getCursorStates(): CursorState[] {
1111
return this._cursor.getCursorStates();
1112
}
1113
public setCursorStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean {
1114
return this._withViewEventsCollector(eventsCollector => this._cursor.setStates(eventsCollector, source, reason, states));
1115
}
1116
public getCursorColumnSelectData(): IColumnSelectData {
1117
return this._cursor.getCursorColumnSelectData();
1118
}
1119
public getCursorAutoClosedCharacters(): Range[] {
1120
return this._cursor.getAutoClosedCharacters();
1121
}
1122
public setCursorColumnSelectData(columnSelectData: IColumnSelectData): void {
1123
this._cursor.setCursorColumnSelectData(columnSelectData);
1124
}
1125
public getPrevEditOperationType(): EditOperationType {
1126
return this._cursor.getPrevEditOperationType();
1127
}
1128
public setPrevEditOperationType(type: EditOperationType): void {
1129
this._cursor.setPrevEditOperationType(type);
1130
}
1131
public getSelection(): Selection {
1132
return this._cursor.getSelection();
1133
}
1134
public getSelections(): Selection[] {
1135
return this._cursor.getSelections();
1136
}
1137
public getPosition(): Position {
1138
return this._cursor.getPrimaryCursorState().modelState.position;
1139
}
1140
public setSelections(source: string | null | undefined, selections: readonly ISelection[], reason = CursorChangeReason.NotSet): void {
1141
this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections, reason));
1142
}
1143
public saveCursorState(): ICursorState[] {
1144
return this._cursor.saveState();
1145
}
1146
public restoreCursorState(states: ICursorState[]): void {
1147
this._withViewEventsCollector(eventsCollector => this._cursor.restoreState(eventsCollector, states));
1148
}
1149
1150
private _executeCursorEdit(callback: (eventsCollector: ViewModelEventsCollector) => void): void {
1151
if (this._cursor.context.cursorConfig.readOnly) {
1152
// we cannot edit when read only...
1153
this._eventDispatcher.emitOutgoingEvent(new ReadOnlyEditAttemptEvent());
1154
return;
1155
}
1156
this._withViewEventsCollector(callback);
1157
}
1158
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, reason: TextModelEditSource): void {
1159
this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer, reason));
1160
}
1161
public startComposition(): void {
1162
this._executeCursorEdit(eventsCollector => this._cursor.startComposition(eventsCollector));
1163
}
1164
public endComposition(source?: string | null | undefined): void {
1165
this._executeCursorEdit(eventsCollector => this._cursor.endComposition(eventsCollector, source));
1166
}
1167
public type(text: string, source?: string | null | undefined): void {
1168
this._executeCursorEdit(eventsCollector => this._cursor.type(eventsCollector, text, source));
1169
}
1170
public compositionType(text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number, source?: string | null | undefined): void {
1171
this._executeCursorEdit(eventsCollector => this._cursor.compositionType(eventsCollector, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta, source));
1172
}
1173
public paste(text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void {
1174
this._executeCursorEdit(eventsCollector => this._cursor.paste(eventsCollector, text, pasteOnNewLine, multicursorText, source));
1175
}
1176
public cut(source?: string | null | undefined): void {
1177
this._executeCursorEdit(eventsCollector => this._cursor.cut(eventsCollector, source));
1178
}
1179
public executeCommand(command: ICommand, source?: string | null | undefined): void {
1180
this._executeCursorEdit(eventsCollector => this._cursor.executeCommand(eventsCollector, command, source));
1181
}
1182
public executeCommands(commands: ICommand[], source?: string | null | undefined): void {
1183
this._executeCursorEdit(eventsCollector => this._cursor.executeCommands(eventsCollector, commands, source));
1184
}
1185
public revealAllCursors(source: string | null | undefined, revealHorizontal: boolean, minimalReveal: boolean = false): void {
1186
this._withViewEventsCollector(eventsCollector => this._cursor.revealAll(eventsCollector, source, minimalReveal, viewEvents.VerticalRevealType.Simple, revealHorizontal, ScrollType.Smooth));
1187
}
1188
public revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean, minimalReveal: boolean = false): void {
1189
this._withViewEventsCollector(eventsCollector => this._cursor.revealPrimary(eventsCollector, source, minimalReveal, viewEvents.VerticalRevealType.Simple, revealHorizontal, ScrollType.Smooth));
1190
}
1191
public revealTopMostCursor(source: string | null | undefined): void {
1192
const viewPosition = this._cursor.getTopMostViewPosition();
1193
const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column);
1194
this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, false, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth)));
1195
}
1196
public revealBottomMostCursor(source: string | null | undefined): void {
1197
const viewPosition = this._cursor.getBottomMostViewPosition();
1198
const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column);
1199
this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, false, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth)));
1200
}
1201
public revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: ScrollType): void {
1202
this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, false, viewRange, null, verticalType, revealHorizontal, scrollType)));
1203
}
1204
1205
//#endregion
1206
1207
//#region viewLayout
1208
public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void {
1209
const hadAChange = this.viewLayout.changeWhitespace(callback);
1210
if (hadAChange) {
1211
this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewZonesChangedEvent());
1212
this._eventDispatcher.emitOutgoingEvent(new ViewZonesChangedEvent());
1213
}
1214
}
1215
//#endregion
1216
1217
private _withViewEventsCollector<T>(callback: (eventsCollector: ViewModelEventsCollector) => T): T {
1218
return this._transactionalTarget.batchChanges(() => {
1219
try {
1220
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
1221
return callback(eventsCollector);
1222
} finally {
1223
this._eventDispatcher.endEmitViewEvents();
1224
}
1225
});
1226
}
1227
1228
public batchEvents(callback: () => void): void {
1229
this._withViewEventsCollector(() => { callback(); });
1230
}
1231
1232
normalizePosition(position: Position, affinity: PositionAffinity): Position {
1233
return this._lines.normalizePosition(position, affinity);
1234
}
1235
1236
/**
1237
* Gets the column at which indentation stops at a given line.
1238
* @internal
1239
*/
1240
getLineIndentColumn(lineNumber: number): number {
1241
return this._lines.getLineIndentColumn(lineNumber);
1242
}
1243
}
1244
1245
export interface IBatchableTarget {
1246
/**
1247
* Allows the target to apply the changes introduced by the callback in a batch.
1248
*/
1249
batchChanges<T>(cb: () => T): T;
1250
}
1251
1252
class ViewportStart implements IDisposable {
1253
1254
public static create(model: ITextModel): ViewportStart {
1255
const viewportStartLineTrackedRange = model._setTrackedRange(null, new Range(1, 1, 1, 1), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges);
1256
return new ViewportStart(model, 1, false, viewportStartLineTrackedRange, 0);
1257
}
1258
1259
public get viewLineNumber(): number {
1260
return this._viewLineNumber;
1261
}
1262
1263
public get isValid(): boolean {
1264
return this._isValid;
1265
}
1266
1267
public get modelTrackedRange(): string {
1268
return this._modelTrackedRange;
1269
}
1270
1271
public get startLineDelta(): number {
1272
return this._startLineDelta;
1273
}
1274
1275
private constructor(
1276
private readonly _model: ITextModel,
1277
private _viewLineNumber: number,
1278
private _isValid: boolean,
1279
private _modelTrackedRange: string,
1280
private _startLineDelta: number,
1281
) { }
1282
1283
public dispose(): void {
1284
this._model._setTrackedRange(this._modelTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges);
1285
}
1286
1287
public update(viewModel: IViewModel, startLineNumber: number): void {
1288
const position = viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, viewModel.getLineMinColumn(startLineNumber)));
1289
const viewportStartLineTrackedRange = viewModel.model._setTrackedRange(this._modelTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges);
1290
const viewportStartLineTop = viewModel.viewLayout.getVerticalOffsetForLineNumber(startLineNumber);
1291
const scrollTop = viewModel.viewLayout.getCurrentScrollTop();
1292
1293
this._viewLineNumber = startLineNumber;
1294
this._isValid = true;
1295
this._modelTrackedRange = viewportStartLineTrackedRange;
1296
this._startLineDelta = scrollTop - viewportStartLineTop;
1297
}
1298
1299
public invalidate(): void {
1300
this._isValid = false;
1301
}
1302
}
1303
1304
class OverviewRulerDecorations {
1305
1306
private readonly _asMap: { [color: string]: OverviewRulerDecorationsGroup } = Object.create(null);
1307
readonly asArray: OverviewRulerDecorationsGroup[] = [];
1308
1309
public accept(color: string, zIndex: number, startLineNumber: number, endLineNumber: number, lane: number): void {
1310
const prevGroup = this._asMap[color];
1311
1312
if (prevGroup) {
1313
const prevData = prevGroup.data;
1314
const prevLane = prevData[prevData.length - 3];
1315
const prevEndLineNumber = prevData[prevData.length - 1];
1316
if (prevLane === lane && prevEndLineNumber + 1 >= startLineNumber) {
1317
// merge into prev
1318
if (endLineNumber > prevEndLineNumber) {
1319
prevData[prevData.length - 1] = endLineNumber;
1320
}
1321
return;
1322
}
1323
1324
// push
1325
prevData.push(lane, startLineNumber, endLineNumber);
1326
} else {
1327
const group = new OverviewRulerDecorationsGroup(color, zIndex, [lane, startLineNumber, endLineNumber]);
1328
this._asMap[color] = group;
1329
this.asArray.push(group);
1330
}
1331
}
1332
}
1333
1334
class HiddenAreasModel {
1335
private readonly hiddenAreas = new Map<unknown, Range[]>();
1336
private shouldRecompute = false;
1337
private ranges: Range[] = [];
1338
1339
setHiddenAreas(source: unknown, ranges: Range[]): void {
1340
const existing = this.hiddenAreas.get(source);
1341
if (existing && rangeArraysEqual(existing, ranges)) {
1342
return;
1343
}
1344
this.hiddenAreas.set(source, ranges);
1345
this.shouldRecompute = true;
1346
}
1347
1348
/**
1349
* The returned array is immutable.
1350
*/
1351
getMergedRanges(): readonly Range[] {
1352
if (!this.shouldRecompute) {
1353
return this.ranges;
1354
}
1355
this.shouldRecompute = false;
1356
const newRanges = Array.from(this.hiddenAreas.values()).reduce((r, hiddenAreas) => mergeLineRangeArray(r, hiddenAreas), []);
1357
if (rangeArraysEqual(this.ranges, newRanges)) {
1358
return this.ranges;
1359
}
1360
this.ranges = newRanges;
1361
return this.ranges;
1362
}
1363
}
1364
1365
function mergeLineRangeArray(arr1: Range[], arr2: Range[]): Range[] {
1366
const result: Range[] = [];
1367
let i = 0;
1368
let j = 0;
1369
while (i < arr1.length && j < arr2.length) {
1370
const item1 = arr1[i];
1371
const item2 = arr2[j];
1372
1373
if (item1.endLineNumber < item2.startLineNumber - 1) {
1374
result.push(arr1[i++]);
1375
} else if (item2.endLineNumber < item1.startLineNumber - 1) {
1376
result.push(arr2[j++]);
1377
} else {
1378
const startLineNumber = Math.min(item1.startLineNumber, item2.startLineNumber);
1379
const endLineNumber = Math.max(item1.endLineNumber, item2.endLineNumber);
1380
result.push(new Range(startLineNumber, 1, endLineNumber, 1));
1381
i++;
1382
j++;
1383
}
1384
}
1385
while (i < arr1.length) {
1386
result.push(arr1[i++]);
1387
}
1388
while (j < arr2.length) {
1389
result.push(arr2[j++]);
1390
}
1391
return result;
1392
}
1393
1394
function rangeArraysEqual(arr1: Range[], arr2: Range[]): boolean {
1395
if (arr1.length !== arr2.length) {
1396
return false;
1397
}
1398
for (let i = 0; i < arr1.length; i++) {
1399
if (!arr1[i].equalsRange(arr2[i])) {
1400
return false;
1401
}
1402
}
1403
return true;
1404
}
1405
1406
/**
1407
* Maintain a stable viewport by trying to keep the first line in the viewport constant.
1408
*/
1409
class StableViewport {
1410
constructor(
1411
public readonly viewportStartModelPosition: Position | null,
1412
public readonly startLineDelta: number
1413
) { }
1414
1415
public recoverViewportStart(coordinatesConverter: ICoordinatesConverter, viewLayout: ViewLayout): void {
1416
if (!this.viewportStartModelPosition) {
1417
return;
1418
}
1419
const viewPosition = coordinatesConverter.convertModelPositionToViewPosition(this.viewportStartModelPosition);
1420
const viewPositionTop = viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber);
1421
viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this.startLineDelta }, ScrollType.Immediate);
1422
}
1423
}
1424
1425