Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/viewModel/modelLineProjection.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 { LineTokens } from '../tokens/lineTokens.js';
7
import { Position } from '../core/position.js';
8
import { IRange } from '../core/range.js';
9
import { EndOfLinePreference, ITextModel, PositionAffinity } from '../model.js';
10
import { LineInjectedText } from '../textModelEvents.js';
11
import { InjectedText, ModelLineProjectionData } from '../modelLineProjectionData.js';
12
import { ViewLineData } from '../viewModel.js';
13
import { SingleLineInlineDecoration } from './inlineDecorations.js';
14
15
export interface IModelLineProjection {
16
isVisible(): boolean;
17
18
/**
19
* This invalidates the current instance (potentially reuses and returns it again).
20
*/
21
setVisible(isVisible: boolean): IModelLineProjection;
22
23
getProjectionData(): ModelLineProjectionData | null;
24
getViewLineCount(): number;
25
getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string;
26
getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;
27
getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;
28
getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;
29
getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData;
30
getViewLinesData(model: ISimpleModel, modelLineNumber: number, outputLineIdx: number, lineCount: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void;
31
32
getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number;
33
getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity?: PositionAffinity): Position;
34
getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number;
35
normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position;
36
37
getInjectedTextAt(outputLineIndex: number, column: number): InjectedText | null;
38
}
39
40
export interface ISimpleModel {
41
tokenization: {
42
getLineTokens(lineNumber: number): LineTokens;
43
};
44
getLineContent(lineNumber: number): string;
45
getLineLength(lineNumber: number): number;
46
getLineMinColumn(lineNumber: number): number;
47
getLineMaxColumn(lineNumber: number): number;
48
getValueInRange(range: IRange, eol?: EndOfLinePreference): string;
49
}
50
51
export function createModelLineProjection(lineBreakData: ModelLineProjectionData | null, isVisible: boolean): IModelLineProjection {
52
if (lineBreakData === null) {
53
// No mapping needed
54
if (isVisible) {
55
return IdentityModelLineProjection.INSTANCE;
56
}
57
return HiddenModelLineProjection.INSTANCE;
58
} else {
59
return new ModelLineProjection(lineBreakData, isVisible);
60
}
61
}
62
63
/**
64
* This projection is used to
65
* * wrap model lines
66
* * inject text
67
*/
68
class ModelLineProjection implements IModelLineProjection {
69
private readonly _projectionData: ModelLineProjectionData;
70
private _isVisible: boolean;
71
72
constructor(lineBreakData: ModelLineProjectionData, isVisible: boolean) {
73
this._projectionData = lineBreakData;
74
this._isVisible = isVisible;
75
}
76
77
public isVisible(): boolean {
78
return this._isVisible;
79
}
80
81
public setVisible(isVisible: boolean): IModelLineProjection {
82
this._isVisible = isVisible;
83
return this;
84
}
85
86
public getProjectionData(): ModelLineProjectionData | null {
87
return this._projectionData;
88
}
89
90
public getViewLineCount(): number {
91
if (!this._isVisible) {
92
return 0;
93
}
94
return this._projectionData.getOutputLineCount();
95
}
96
97
public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string {
98
this._assertVisible();
99
100
const startOffsetInInputWithInjections = outputLineIndex > 0 ? this._projectionData.breakOffsets[outputLineIndex - 1] : 0;
101
const endOffsetInInputWithInjections = this._projectionData.breakOffsets[outputLineIndex];
102
103
let r: string;
104
if (this._projectionData.injectionOffsets !== null) {
105
const injectedTexts = this._projectionData.injectionOffsets.map(
106
(offset, idx) => new LineInjectedText(
107
0,
108
0,
109
offset + 1,
110
this._projectionData.injectionOptions![idx],
111
0
112
)
113
);
114
const lineWithInjections = LineInjectedText.applyInjectedText(
115
model.getLineContent(modelLineNumber),
116
injectedTexts
117
);
118
r = lineWithInjections.substring(startOffsetInInputWithInjections, endOffsetInInputWithInjections);
119
} else {
120
r = model.getValueInRange({
121
startLineNumber: modelLineNumber,
122
startColumn: startOffsetInInputWithInjections + 1,
123
endLineNumber: modelLineNumber,
124
endColumn: endOffsetInInputWithInjections + 1
125
});
126
}
127
128
if (outputLineIndex > 0) {
129
r = spaces(this._projectionData.wrappedTextIndentLength) + r;
130
}
131
132
return r;
133
}
134
135
public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {
136
this._assertVisible();
137
return this._projectionData.getLineLength(outputLineIndex);
138
}
139
140
public getViewLineMinColumn(_model: ITextModel, _modelLineNumber: number, outputLineIndex: number): number {
141
this._assertVisible();
142
return this._projectionData.getMinOutputOffset(outputLineIndex) + 1;
143
}
144
145
public getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {
146
this._assertVisible();
147
return this._projectionData.getMaxOutputOffset(outputLineIndex) + 1;
148
}
149
150
/**
151
* Try using {@link getViewLinesData} instead.
152
*/
153
public getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData {
154
const arr = new Array<ViewLineData>();
155
this.getViewLinesData(model, modelLineNumber, outputLineIndex, 1, 0, [true], arr);
156
return arr[0];
157
}
158
159
public getViewLinesData(model: ISimpleModel, modelLineNumber: number, outputLineIdx: number, lineCount: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void {
160
this._assertVisible();
161
162
const lineBreakData = this._projectionData;
163
164
const injectionOffsets = lineBreakData.injectionOffsets;
165
const injectionOptions = lineBreakData.injectionOptions;
166
167
let inlineDecorationsPerOutputLine: SingleLineInlineDecoration[][] | null = null;
168
169
if (injectionOffsets) {
170
inlineDecorationsPerOutputLine = [];
171
let totalInjectedTextLengthBefore = 0;
172
let currentInjectedOffset = 0;
173
174
for (let outputLineIndex = 0; outputLineIndex < lineBreakData.getOutputLineCount(); outputLineIndex++) {
175
const inlineDecorations = new Array<SingleLineInlineDecoration>();
176
inlineDecorationsPerOutputLine[outputLineIndex] = inlineDecorations;
177
178
const lineStartOffsetInInputWithInjections = outputLineIndex > 0 ? lineBreakData.breakOffsets[outputLineIndex - 1] : 0;
179
const lineEndOffsetInInputWithInjections = lineBreakData.breakOffsets[outputLineIndex];
180
181
while (currentInjectedOffset < injectionOffsets.length) {
182
const length = injectionOptions![currentInjectedOffset].content.length;
183
const injectedTextStartOffsetInInputWithInjections = injectionOffsets[currentInjectedOffset] + totalInjectedTextLengthBefore;
184
const injectedTextEndOffsetInInputWithInjections = injectedTextStartOffsetInInputWithInjections + length;
185
186
if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections) {
187
// Injected text only starts in later wrapped lines.
188
break;
189
}
190
191
if (lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections) {
192
// Injected text ends after or in this line (but also starts in or before this line).
193
const options = injectionOptions![currentInjectedOffset];
194
if (options.inlineClassName) {
195
const offset = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);
196
const start = offset + Math.max(injectedTextStartOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, 0);
197
const end = offset + Math.min(injectedTextEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections);
198
if (start !== end) {
199
inlineDecorations.push(new SingleLineInlineDecoration(start, end, options.inlineClassName, options.inlineClassNameAffectsLetterSpacing!));
200
}
201
}
202
}
203
204
if (injectedTextEndOffsetInInputWithInjections <= lineEndOffsetInInputWithInjections) {
205
totalInjectedTextLengthBefore += length;
206
currentInjectedOffset++;
207
} else {
208
// injected text breaks into next line, process it again
209
break;
210
}
211
}
212
}
213
}
214
215
let lineWithInjections: LineTokens;
216
if (injectionOffsets) {
217
const tokensToInsert: { offset: number; text: string; tokenMetadata: number }[] = [];
218
219
for (let idx = 0; idx < injectionOffsets.length; idx++) {
220
const offset = injectionOffsets[idx];
221
const tokens = injectionOptions![idx].tokens;
222
if (tokens) {
223
tokens.forEach((range, info) => {
224
tokensToInsert.push({
225
offset,
226
text: range.substring(injectionOptions![idx].content),
227
tokenMetadata: info.metadata,
228
});
229
});
230
} else {
231
tokensToInsert.push({
232
offset,
233
text: injectionOptions![idx].content,
234
tokenMetadata: LineTokens.defaultTokenMetadata,
235
});
236
}
237
}
238
239
lineWithInjections = model.tokenization.getLineTokens(modelLineNumber).withInserted(tokensToInsert);
240
} else {
241
lineWithInjections = model.tokenization.getLineTokens(modelLineNumber);
242
}
243
244
for (let outputLineIndex = outputLineIdx; outputLineIndex < outputLineIdx + lineCount; outputLineIndex++) {
245
const globalIndex = globalStartIndex + outputLineIndex - outputLineIdx;
246
if (!needed[globalIndex]) {
247
result[globalIndex] = null;
248
continue;
249
}
250
result[globalIndex] = this._getViewLineData(lineWithInjections, inlineDecorationsPerOutputLine ? inlineDecorationsPerOutputLine[outputLineIndex] : null, outputLineIndex);
251
}
252
}
253
254
private _getViewLineData(lineWithInjections: LineTokens, inlineDecorations: null | SingleLineInlineDecoration[], outputLineIndex: number): ViewLineData {
255
this._assertVisible();
256
const lineBreakData = this._projectionData;
257
const deltaStartIndex = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);
258
259
const lineStartOffsetInInputWithInjections = outputLineIndex > 0 ? lineBreakData.breakOffsets[outputLineIndex - 1] : 0;
260
const lineEndOffsetInInputWithInjections = lineBreakData.breakOffsets[outputLineIndex];
261
const tokens = lineWithInjections.sliceAndInflate(lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections, deltaStartIndex);
262
263
let lineContent = tokens.getLineContent();
264
if (outputLineIndex > 0) {
265
lineContent = spaces(lineBreakData.wrappedTextIndentLength) + lineContent;
266
}
267
268
const minColumn = this._projectionData.getMinOutputOffset(outputLineIndex) + 1;
269
const maxColumn = lineContent.length + 1;
270
const continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount());
271
const startVisibleColumn = (outputLineIndex === 0 ? 0 : lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]);
272
273
return new ViewLineData(
274
lineContent,
275
continuesWithWrappedLine,
276
minColumn,
277
maxColumn,
278
startVisibleColumn,
279
tokens,
280
inlineDecorations
281
);
282
}
283
284
public getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number {
285
this._assertVisible();
286
return this._projectionData.translateToInputOffset(outputLineIndex, outputColumn - 1) + 1;
287
}
288
289
public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position {
290
this._assertVisible();
291
const r = this._projectionData.translateToOutputPosition(inputColumn - 1, affinity);
292
return r.toPosition(deltaLineNumber);
293
}
294
295
public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number {
296
this._assertVisible();
297
const r = this._projectionData.translateToOutputPosition(inputColumn - 1);
298
return deltaLineNumber + r.outputLineIndex;
299
}
300
301
public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
302
const baseViewLineNumber = outputPosition.lineNumber - outputLineIndex;
303
const normalizedOutputPosition = this._projectionData.normalizeOutputPosition(outputLineIndex, outputPosition.column - 1, affinity);
304
const result = normalizedOutputPosition.toPosition(baseViewLineNumber);
305
return result;
306
}
307
308
public getInjectedTextAt(outputLineIndex: number, outputColumn: number): InjectedText | null {
309
return this._projectionData.getInjectedText(outputLineIndex, outputColumn - 1);
310
}
311
312
private _assertVisible() {
313
if (!this._isVisible) {
314
throw new Error('Not supported');
315
}
316
}
317
}
318
319
/**
320
* This projection does not change the model line.
321
*/
322
class IdentityModelLineProjection implements IModelLineProjection {
323
public static readonly INSTANCE = new IdentityModelLineProjection();
324
325
private constructor() { }
326
327
public isVisible(): boolean {
328
return true;
329
}
330
331
public setVisible(isVisible: boolean): IModelLineProjection {
332
if (isVisible) {
333
return this;
334
}
335
return HiddenModelLineProjection.INSTANCE;
336
}
337
338
public getProjectionData(): ModelLineProjectionData | null {
339
return null;
340
}
341
342
public getViewLineCount(): number {
343
return 1;
344
}
345
346
public getViewLineContent(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): string {
347
return model.getLineContent(modelLineNumber);
348
}
349
350
public getViewLineLength(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {
351
return model.getLineLength(modelLineNumber);
352
}
353
354
public getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {
355
return model.getLineMinColumn(modelLineNumber);
356
}
357
358
public getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {
359
return model.getLineMaxColumn(modelLineNumber);
360
}
361
362
public getViewLineData(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): ViewLineData {
363
const lineTokens = model.tokenization.getLineTokens(modelLineNumber);
364
const lineContent = lineTokens.getLineContent();
365
return new ViewLineData(
366
lineContent,
367
false,
368
1,
369
lineContent.length + 1,
370
0,
371
lineTokens.inflate(),
372
null
373
);
374
}
375
376
public getViewLinesData(model: ISimpleModel, modelLineNumber: number, _fromOuputLineIndex: number, _toOutputLineIndex: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void {
377
if (!needed[globalStartIndex]) {
378
result[globalStartIndex] = null;
379
return;
380
}
381
result[globalStartIndex] = this.getViewLineData(model, modelLineNumber, 0);
382
}
383
384
public getModelColumnOfViewPosition(_outputLineIndex: number, outputColumn: number): number {
385
return outputColumn;
386
}
387
388
public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position {
389
return new Position(deltaLineNumber, inputColumn);
390
}
391
392
public getViewLineNumberOfModelPosition(deltaLineNumber: number, _inputColumn: number): number {
393
return deltaLineNumber;
394
}
395
396
public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
397
return outputPosition;
398
}
399
400
public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {
401
return null;
402
}
403
}
404
405
/**
406
* This projection hides the model line.
407
*/
408
class HiddenModelLineProjection implements IModelLineProjection {
409
public static readonly INSTANCE = new HiddenModelLineProjection();
410
411
private constructor() { }
412
413
public isVisible(): boolean {
414
return false;
415
}
416
417
public setVisible(isVisible: boolean): IModelLineProjection {
418
if (!isVisible) {
419
return this;
420
}
421
return IdentityModelLineProjection.INSTANCE;
422
}
423
424
public getProjectionData(): ModelLineProjectionData | null {
425
return null;
426
}
427
428
public getViewLineCount(): number {
429
return 0;
430
}
431
432
public getViewLineContent(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): string {
433
throw new Error('Not supported');
434
}
435
436
public getViewLineLength(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {
437
throw new Error('Not supported');
438
}
439
440
public getViewLineMinColumn(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {
441
throw new Error('Not supported');
442
}
443
444
public getViewLineMaxColumn(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {
445
throw new Error('Not supported');
446
}
447
448
public getViewLineData(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): ViewLineData {
449
throw new Error('Not supported');
450
}
451
452
public getViewLinesData(_model: ISimpleModel, _modelLineNumber: number, _fromOuputLineIndex: number, _toOutputLineIndex: number, _globalStartIndex: number, _needed: boolean[], _result: ViewLineData[]): void {
453
throw new Error('Not supported');
454
}
455
456
public getModelColumnOfViewPosition(_outputLineIndex: number, _outputColumn: number): number {
457
throw new Error('Not supported');
458
}
459
460
public getViewPositionOfModelPosition(_deltaLineNumber: number, _inputColumn: number): Position {
461
throw new Error('Not supported');
462
}
463
464
public getViewLineNumberOfModelPosition(_deltaLineNumber: number, _inputColumn: number): number {
465
throw new Error('Not supported');
466
}
467
468
public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
469
throw new Error('Not supported');
470
}
471
472
public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {
473
throw new Error('Not supported');
474
}
475
}
476
477
const _spaces: string[] = [''];
478
function spaces(count: number): string {
479
if (count >= _spaces.length) {
480
for (let i = 1; i <= count; i++) {
481
_spaces[i] = _makeSpaces(i);
482
}
483
}
484
return _spaces[count];
485
}
486
487
function _makeSpaces(count: number): string {
488
return new Array(count + 1).join(' ');
489
}
490
491