Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/view/domLineBreaksComputer.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 { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';
7
import { CharCode } from '../../../base/common/charCode.js';
8
import * as strings from '../../../base/common/strings.js';
9
import { assertReturnsDefined } from '../../../base/common/types.js';
10
import { applyFontInfo } from '../config/domFontInfo.js';
11
import { WrappingIndent } from '../../common/config/editorOptions.js';
12
import { FontInfo } from '../../common/config/fontInfo.js';
13
import { StringBuilder } from '../../common/core/stringBuilder.js';
14
import { InjectedTextOptions } from '../../common/model.js';
15
import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js';
16
import { LineInjectedText } from '../../common/textModelEvents.js';
17
18
const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value });
19
20
export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory {
21
22
public static create(targetWindow: Window): DOMLineBreaksComputerFactory {
23
return new DOMLineBreaksComputerFactory(new WeakRef(targetWindow));
24
}
25
26
constructor(private targetWindow: WeakRef<Window>) {
27
}
28
29
public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer {
30
const requests: string[] = [];
31
const injectedTexts: (LineInjectedText[] | null)[] = [];
32
return {
33
addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => {
34
requests.push(lineText);
35
injectedTexts.push(injectedText);
36
},
37
finalize: () => {
38
return createLineBreaks(assertReturnsDefined(this.targetWindow.deref()), requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts);
39
}
40
};
41
}
42
}
43
44
function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', injectedTextsPerLine: (LineInjectedText[] | null)[]): (ModelLineProjectionData | null)[] {
45
function createEmptyLineBreakWithPossiblyInjectedText(requestIdx: number): ModelLineProjectionData | null {
46
const injectedTexts = injectedTextsPerLine[requestIdx];
47
if (injectedTexts) {
48
const lineText = LineInjectedText.applyInjectedText(requests[requestIdx], injectedTexts);
49
50
const injectionOptions = injectedTexts.map(t => t.options);
51
const injectionOffsets = injectedTexts.map(text => text.column - 1);
52
53
// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK
54
// because `breakOffsetsVisibleColumn` will never be used because it contains injected text
55
return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);
56
} else {
57
return null;
58
}
59
}
60
61
if (firstLineBreakColumn === -1) {
62
const result: (ModelLineProjectionData | null)[] = [];
63
for (let i = 0, len = requests.length; i < len; i++) {
64
result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
65
}
66
return result;
67
}
68
69
const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth);
70
const additionalIndent = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0);
71
const additionalIndentSize = Math.round(tabSize * additionalIndent);
72
const additionalIndentLength = Math.ceil(fontInfo.spaceWidth * additionalIndentSize);
73
74
const containerDomNode = document.createElement('div');
75
applyFontInfo(containerDomNode, fontInfo);
76
77
const sb = new StringBuilder(10000);
78
const firstNonWhitespaceIndices: number[] = [];
79
const wrappedTextIndentLengths: number[] = [];
80
const renderLineContents: string[] = [];
81
const allCharOffsets: number[][] = [];
82
const allVisibleColumns: number[][] = [];
83
for (let i = 0; i < requests.length; i++) {
84
const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]);
85
86
let firstNonWhitespaceIndex = 0;
87
let wrappedTextIndentLength = 0;
88
let width = overallWidth;
89
90
if (wrappingIndent !== WrappingIndent.None) {
91
firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
92
if (firstNonWhitespaceIndex === -1) {
93
// all whitespace line
94
firstNonWhitespaceIndex = 0;
95
96
} else {
97
// Track existing indent
98
99
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
100
const charWidth = (
101
lineContent.charCodeAt(i) === CharCode.Tab
102
? (tabSize - (wrappedTextIndentLength % tabSize))
103
: 1
104
);
105
wrappedTextIndentLength += charWidth;
106
}
107
108
const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength);
109
110
// Force sticking to beginning of line if no character would fit except for the indentation
111
if (indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth) {
112
firstNonWhitespaceIndex = 0;
113
wrappedTextIndentLength = 0;
114
} else {
115
width = overallWidth - indentWidth;
116
}
117
}
118
}
119
120
const renderLineContent = lineContent.substr(firstNonWhitespaceIndex);
121
const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength);
122
firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex;
123
wrappedTextIndentLengths[i] = wrappedTextIndentLength;
124
renderLineContents[i] = renderLineContent;
125
allCharOffsets[i] = tmp[0];
126
allVisibleColumns[i] = tmp[1];
127
}
128
const html = sb.build();
129
const trustedhtml = ttPolicy?.createHTML(html) ?? html;
130
containerDomNode.innerHTML = trustedhtml as string;
131
132
containerDomNode.style.position = 'absolute';
133
containerDomNode.style.top = '10000';
134
if (wordBreak === 'keepAll') {
135
// word-break: keep-all; overflow-wrap: anywhere
136
containerDomNode.style.wordBreak = 'keep-all';
137
containerDomNode.style.overflowWrap = 'anywhere';
138
} else {
139
// overflow-wrap: break-word
140
containerDomNode.style.wordBreak = 'inherit';
141
containerDomNode.style.overflowWrap = 'break-word';
142
}
143
targetWindow.document.body.appendChild(containerDomNode);
144
145
const range = document.createRange();
146
const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0);
147
148
const result: (ModelLineProjectionData | null)[] = [];
149
for (let i = 0; i < requests.length; i++) {
150
const lineDomNode = lineDomNodes[i];
151
const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]);
152
if (breakOffsets === null) {
153
result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
154
continue;
155
}
156
157
const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i];
158
const wrappedTextIndentLength = wrappedTextIndentLengths[i] + additionalIndentSize;
159
const visibleColumns = allVisibleColumns[i];
160
161
const breakOffsetsVisibleColumn: number[] = [];
162
for (let j = 0, len = breakOffsets.length; j < len; j++) {
163
breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]];
164
}
165
166
if (firstNonWhitespaceIndex !== 0) {
167
// All break offsets are relative to the renderLineContent, make them absolute again
168
for (let j = 0, len = breakOffsets.length; j < len; j++) {
169
breakOffsets[j] += firstNonWhitespaceIndex;
170
}
171
}
172
173
let injectionOptions: InjectedTextOptions[] | null;
174
let injectionOffsets: number[] | null;
175
const curInjectedTexts = injectedTextsPerLine[i];
176
if (curInjectedTexts) {
177
injectionOptions = curInjectedTexts.map(t => t.options);
178
injectionOffsets = curInjectedTexts.map(text => text.column - 1);
179
} else {
180
injectionOptions = null;
181
injectionOffsets = null;
182
}
183
184
result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength);
185
}
186
187
containerDomNode.remove();
188
return result;
189
}
190
191
const enum Constants {
192
SPAN_MODULO_LIMIT = 16384
193
}
194
195
function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[]] {
196
197
if (wrappingIndentLength !== 0) {
198
const hangingOffset = String(wrappingIndentLength);
199
sb.appendString('<div style="text-indent: -');
200
sb.appendString(hangingOffset);
201
sb.appendString('px; padding-left: ');
202
sb.appendString(hangingOffset);
203
sb.appendString('px; box-sizing: border-box; width:');
204
} else {
205
sb.appendString('<div style="width:');
206
}
207
sb.appendString(String(width));
208
sb.appendString('px;">');
209
// if (containsRTL) {
210
// sb.appendASCIIString('" dir="ltr');
211
// }
212
213
const len = lineContent.length;
214
let visibleColumn = initialVisibleColumn;
215
let charOffset = 0;
216
const charOffsets: number[] = [];
217
const visibleColumns: number[] = [];
218
let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null);
219
220
sb.appendString('<span>');
221
for (let charIndex = 0; charIndex < len; charIndex++) {
222
if (charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) {
223
sb.appendString('</span><span>');
224
}
225
charOffsets[charIndex] = charOffset;
226
visibleColumns[charIndex] = visibleColumn;
227
const charCode = nextCharCode;
228
nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);
229
let producedCharacters = 1;
230
let charWidth = 1;
231
switch (charCode) {
232
case CharCode.Tab:
233
producedCharacters = (tabSize - (visibleColumn % tabSize));
234
charWidth = producedCharacters;
235
for (let space = 1; space <= producedCharacters; space++) {
236
if (space < producedCharacters) {
237
sb.appendCharCode(0xA0); // &nbsp;
238
} else {
239
sb.appendASCIICharCode(CharCode.Space);
240
}
241
}
242
break;
243
244
case CharCode.Space:
245
if (nextCharCode === CharCode.Space) {
246
sb.appendCharCode(0xA0); // &nbsp;
247
} else {
248
sb.appendASCIICharCode(CharCode.Space);
249
}
250
break;
251
252
case CharCode.LessThan:
253
sb.appendString('&lt;');
254
break;
255
256
case CharCode.GreaterThan:
257
sb.appendString('&gt;');
258
break;
259
260
case CharCode.Ampersand:
261
sb.appendString('&amp;');
262
break;
263
264
case CharCode.Null:
265
sb.appendString('&#00;');
266
break;
267
268
case CharCode.UTF8_BOM:
269
case CharCode.LINE_SEPARATOR:
270
case CharCode.PARAGRAPH_SEPARATOR:
271
case CharCode.NEXT_LINE:
272
sb.appendCharCode(0xFFFD);
273
break;
274
275
default:
276
if (strings.isFullWidthCharacter(charCode)) {
277
charWidth++;
278
}
279
if (charCode < 32) {
280
sb.appendCharCode(9216 + charCode);
281
} else {
282
sb.appendCharCode(charCode);
283
}
284
}
285
286
charOffset += producedCharacters;
287
visibleColumn += charWidth;
288
}
289
sb.appendString('</span>');
290
291
charOffsets[lineContent.length] = charOffset;
292
visibleColumns[lineContent.length] = visibleColumn;
293
294
sb.appendString('</div>');
295
296
return [charOffsets, visibleColumns];
297
}
298
299
function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null {
300
if (lineContent.length <= 1) {
301
return null;
302
}
303
const spans = <HTMLSpanElement[]>Array.prototype.slice.call(lineDomNode.children, 0);
304
305
const breakOffsets: number[] = [];
306
try {
307
discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets);
308
} catch (err) {
309
console.log(err);
310
return null;
311
}
312
313
if (breakOffsets.length === 0) {
314
return null;
315
}
316
317
breakOffsets.push(lineContent.length);
318
return breakOffsets;
319
}
320
321
function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void {
322
if (low === high) {
323
return;
324
}
325
326
lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]);
327
highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]);
328
329
if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) {
330
// same line
331
return;
332
}
333
334
// there is at least one line break between these two offsets
335
if (low + 1 === high) {
336
// the two characters are adjacent, so the line break must be exactly between them
337
result.push(high);
338
return;
339
}
340
341
const mid = low + ((high - low) / 2) | 0;
342
const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]);
343
discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result);
344
discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result);
345
}
346
347
function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number): DOMRectList {
348
range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT);
349
range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT);
350
return range.getClientRects();
351
}
352
353