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