Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/modelLineProjectionData.ts
3292 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 { assertNever } from '../../base/common/assert.js';
7
import { WrappingIndent } from './config/editorOptions.js';
8
import { FontInfo } from './config/fontInfo.js';
9
import { Position } from './core/position.js';
10
import { InjectedTextCursorStops, InjectedTextOptions, PositionAffinity } from './model.js';
11
import { LineInjectedText } from './textModelEvents.js';
12
13
/**
14
* *input*:
15
* ```
16
* xxxxxxxxxxxxxxxxxxxxxxxxxxx
17
* ```
18
*
19
* -> Applying injections `[i...i]`, *inputWithInjections*:
20
* ```
21
* xxxxxx[iiiiiiiiii]xxxxxxxxxxxxxxxxx[ii]xxxx
22
* ```
23
*
24
* -> breaking at offsets `|` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xxxxxx[ii]xxxx|`:
25
* ```
26
* xxxxxx[iiiiiii
27
* iii]xxxxxxxxxxx
28
* xxxxxx[ii]xxxx
29
* ```
30
*
31
* -> applying wrappedTextIndentLength, *output*:
32
* ```
33
* xxxxxx[iiiiiii
34
* iii]xxxxxxxxxxx
35
* xxxxxx[ii]xxxx
36
* ```
37
*/
38
export class ModelLineProjectionData {
39
constructor(
40
public injectionOffsets: number[] | null,
41
/**
42
* `injectionOptions.length` must equal `injectionOffsets.length`
43
*/
44
public injectionOptions: InjectedTextOptions[] | null,
45
/**
46
* Refers to offsets after applying injections to the source.
47
* The last break offset indicates the length of the source after applying injections.
48
*/
49
public breakOffsets: number[],
50
/**
51
* Refers to offsets after applying injections
52
*/
53
public breakOffsetsVisibleColumn: number[],
54
public wrappedTextIndentLength: number
55
) {
56
}
57
58
public getOutputLineCount(): number {
59
return this.breakOffsets.length;
60
}
61
62
public getMinOutputOffset(outputLineIndex: number): number {
63
if (outputLineIndex > 0) {
64
return this.wrappedTextIndentLength;
65
}
66
return 0;
67
}
68
69
public getLineLength(outputLineIndex: number): number {
70
// These offsets refer to model text with injected text.
71
const startOffset = outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0;
72
const endOffset = this.breakOffsets[outputLineIndex];
73
74
let lineLength = endOffset - startOffset;
75
if (outputLineIndex > 0) {
76
lineLength += this.wrappedTextIndentLength;
77
}
78
return lineLength;
79
}
80
81
public getMaxOutputOffset(outputLineIndex: number): number {
82
return this.getLineLength(outputLineIndex);
83
}
84
85
public translateToInputOffset(outputLineIndex: number, outputOffset: number): number {
86
if (outputLineIndex > 0) {
87
outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);
88
}
89
90
const offsetInInputWithInjection = outputLineIndex === 0 ? outputOffset : this.breakOffsets[outputLineIndex - 1] + outputOffset;
91
let offsetInInput = offsetInInputWithInjection;
92
93
if (this.injectionOffsets !== null) {
94
for (let i = 0; i < this.injectionOffsets.length; i++) {
95
if (offsetInInput > this.injectionOffsets[i]) {
96
if (offsetInInput < this.injectionOffsets[i] + this.injectionOptions![i].content.length) {
97
// `inputOffset` is within injected text
98
offsetInInput = this.injectionOffsets[i];
99
} else {
100
offsetInInput -= this.injectionOptions![i].content.length;
101
}
102
} else {
103
break;
104
}
105
}
106
}
107
108
return offsetInInput;
109
}
110
111
public translateToOutputPosition(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {
112
let inputOffsetInInputWithInjection = inputOffset;
113
if (this.injectionOffsets !== null) {
114
for (let i = 0; i < this.injectionOffsets.length; i++) {
115
if (inputOffset < this.injectionOffsets[i]) {
116
break;
117
}
118
119
if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) {
120
break;
121
}
122
123
inputOffsetInInputWithInjection += this.injectionOptions![i].content.length;
124
}
125
}
126
127
return this.offsetInInputWithInjectionsToOutputPosition(inputOffsetInInputWithInjection, affinity);
128
}
129
130
private offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {
131
let low = 0;
132
let high = this.breakOffsets.length - 1;
133
let mid = 0;
134
let midStart = 0;
135
136
while (low <= high) {
137
mid = low + ((high - low) / 2) | 0;
138
139
const midStop = this.breakOffsets[mid];
140
midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0;
141
142
if (affinity === PositionAffinity.Left) {
143
if (offsetInInputWithInjections <= midStart) {
144
high = mid - 1;
145
} else if (offsetInInputWithInjections > midStop) {
146
low = mid + 1;
147
} else {
148
break;
149
}
150
} else {
151
if (offsetInInputWithInjections < midStart) {
152
high = mid - 1;
153
} else if (offsetInInputWithInjections >= midStop) {
154
low = mid + 1;
155
} else {
156
break;
157
}
158
}
159
}
160
161
let outputOffset = offsetInInputWithInjections - midStart;
162
if (mid > 0) {
163
outputOffset += this.wrappedTextIndentLength;
164
}
165
166
return new OutputPosition(mid, outputOffset);
167
}
168
169
public normalizeOutputPosition(outputLineIndex: number, outputOffset: number, affinity: PositionAffinity): OutputPosition {
170
if (this.injectionOffsets !== null) {
171
const offsetInInputWithInjections = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);
172
const normalizedOffsetInUnwrappedLine = this.normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity);
173
if (normalizedOffsetInUnwrappedLine !== offsetInInputWithInjections) {
174
// injected text caused a change
175
return this.offsetInInputWithInjectionsToOutputPosition(normalizedOffsetInUnwrappedLine, affinity);
176
}
177
}
178
179
if (affinity === PositionAffinity.Left) {
180
if (outputLineIndex > 0 && outputOffset === this.getMinOutputOffset(outputLineIndex)) {
181
return new OutputPosition(outputLineIndex - 1, this.getMaxOutputOffset(outputLineIndex - 1));
182
}
183
}
184
else if (affinity === PositionAffinity.Right) {
185
const maxOutputLineIndex = this.getOutputLineCount() - 1;
186
if (outputLineIndex < maxOutputLineIndex && outputOffset === this.getMaxOutputOffset(outputLineIndex)) {
187
return new OutputPosition(outputLineIndex + 1, this.getMinOutputOffset(outputLineIndex + 1));
188
}
189
}
190
191
return new OutputPosition(outputLineIndex, outputOffset);
192
}
193
194
private outputPositionToOffsetInInputWithInjections(outputLineIndex: number, outputOffset: number): number {
195
if (outputLineIndex > 0) {
196
outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);
197
}
198
const result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset;
199
return result;
200
}
201
202
private normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections: number, affinity: PositionAffinity): number {
203
const injectedText = this.getInjectedTextAtOffset(offsetInInputWithInjections);
204
if (!injectedText) {
205
return offsetInInputWithInjections;
206
}
207
208
if (affinity === PositionAffinity.None) {
209
if (offsetInInputWithInjections === injectedText.offsetInInputWithInjections + injectedText.length
210
&& hasRightCursorStop(this.injectionOptions![injectedText.injectedTextIndex].cursorStops)) {
211
return injectedText.offsetInInputWithInjections + injectedText.length;
212
} else {
213
let result = injectedText.offsetInInputWithInjections;
214
if (hasLeftCursorStop(this.injectionOptions![injectedText.injectedTextIndex].cursorStops)) {
215
return result;
216
}
217
218
let index = injectedText.injectedTextIndex - 1;
219
while (index >= 0 && this.injectionOffsets![index] === this.injectionOffsets![injectedText.injectedTextIndex]) {
220
if (hasRightCursorStop(this.injectionOptions![index].cursorStops)) {
221
break;
222
}
223
result -= this.injectionOptions![index].content.length;
224
if (hasLeftCursorStop(this.injectionOptions![index].cursorStops)) {
225
break;
226
}
227
index--;
228
}
229
230
return result;
231
}
232
} else if (affinity === PositionAffinity.Right || affinity === PositionAffinity.RightOfInjectedText) {
233
let result = injectedText.offsetInInputWithInjections + injectedText.length;
234
let index = injectedText.injectedTextIndex;
235
// traverse all injected text that touch each other
236
while (index + 1 < this.injectionOffsets!.length && this.injectionOffsets![index + 1] === this.injectionOffsets![index]) {
237
result += this.injectionOptions![index + 1].content.length;
238
index++;
239
}
240
return result;
241
} else if (affinity === PositionAffinity.Left || affinity === PositionAffinity.LeftOfInjectedText) {
242
// affinity is left
243
let result = injectedText.offsetInInputWithInjections;
244
let index = injectedText.injectedTextIndex;
245
// traverse all injected text that touch each other
246
while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) {
247
result -= this.injectionOptions![index - 1].content.length;
248
index--;
249
}
250
return result;
251
}
252
253
assertNever(affinity);
254
}
255
256
public getInjectedText(outputLineIndex: number, outputOffset: number): InjectedText | null {
257
const offset = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);
258
const injectedText = this.getInjectedTextAtOffset(offset);
259
if (!injectedText) {
260
return null;
261
}
262
return {
263
options: this.injectionOptions![injectedText.injectedTextIndex]
264
};
265
}
266
267
private getInjectedTextAtOffset(offsetInInputWithInjections: number): { injectedTextIndex: number; offsetInInputWithInjections: number; length: number } | undefined {
268
const injectionOffsets = this.injectionOffsets;
269
const injectionOptions = this.injectionOptions;
270
271
if (injectionOffsets !== null) {
272
let totalInjectedTextLengthBefore = 0;
273
for (let i = 0; i < injectionOffsets.length; i++) {
274
const length = injectionOptions![i].content.length;
275
const injectedTextStartOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore;
276
const injectedTextEndOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore + length;
277
278
if (injectedTextStartOffsetInInputWithInjections > offsetInInputWithInjections) {
279
// Injected text starts later.
280
break; // All later injected texts have an even larger offset.
281
}
282
283
if (offsetInInputWithInjections <= injectedTextEndOffsetInInputWithInjections) {
284
// Injected text ends after or with the given position (but also starts with or before it).
285
return {
286
injectedTextIndex: i,
287
offsetInInputWithInjections: injectedTextStartOffsetInInputWithInjections,
288
length
289
};
290
}
291
292
totalInjectedTextLengthBefore += length;
293
}
294
}
295
296
return undefined;
297
}
298
}
299
300
function hasRightCursorStop(cursorStop: InjectedTextCursorStops | null | undefined): boolean {
301
if (cursorStop === null || cursorStop === undefined) { return true; }
302
return cursorStop === InjectedTextCursorStops.Right || cursorStop === InjectedTextCursorStops.Both;
303
}
304
function hasLeftCursorStop(cursorStop: InjectedTextCursorStops | null | undefined): boolean {
305
if (cursorStop === null || cursorStop === undefined) { return true; }
306
return cursorStop === InjectedTextCursorStops.Left || cursorStop === InjectedTextCursorStops.Both;
307
}
308
309
export class InjectedText {
310
constructor(public readonly options: InjectedTextOptions) { }
311
}
312
313
export class OutputPosition {
314
outputLineIndex: number;
315
outputOffset: number;
316
317
constructor(outputLineIndex: number, outputOffset: number) {
318
this.outputLineIndex = outputLineIndex;
319
this.outputOffset = outputOffset;
320
}
321
322
toString(): string {
323
return `${this.outputLineIndex}:${this.outputOffset}`;
324
}
325
326
toPosition(baseLineNumber: number): Position {
327
return new Position(baseLineNumber + this.outputLineIndex, this.outputOffset + 1);
328
}
329
}
330
331
export interface ILineBreaksComputerFactory {
332
createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer;
333
}
334
335
export interface ILineBreaksComputer {
336
/**
337
* Pass in `previousLineBreakData` if the only difference is in breaking columns!!!
338
*/
339
addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void;
340
finalize(): (ModelLineProjectionData | null)[];
341
}
342
343