Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
3296 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 * as browser from '../../../../base/browser/browser.js';
7
import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';
8
import * as platform from '../../../../base/common/platform.js';
9
import { IVisibleLine } from '../../view/viewLayer.js';
10
import { RangeUtil } from './rangeUtil.js';
11
import { StringBuilder } from '../../../common/core/stringBuilder.js';
12
import { FloatHorizontalRange, VisibleRanges } from '../../view/renderingContext.js';
13
import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js';
14
import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, DomPosition, RenderWhitespace } from '../../../common/viewLayout/viewLineRenderer.js';
15
import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
16
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
17
import { EditorFontLigatures } from '../../../common/config/editorOptions.js';
18
import { DomReadingContext } from './domReadingContext.js';
19
import type { ViewLineOptions } from './viewLineOptions.js';
20
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
21
import { OffsetRange } from '../../../common/core/ranges/offsetRange.js';
22
import { InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js';
23
import { TextDirection } from '../../../common/model.js';
24
25
const canUseFastRenderedViewLine = (function () {
26
if (platform.isNative) {
27
// In VSCode we know very well when the zoom level changes
28
return true;
29
}
30
31
if (platform.isLinux || browser.isFirefox || browser.isSafari) {
32
// On Linux, it appears that zooming affects char widths (in pixels), which is unexpected.
33
// --
34
// Even though we read character widths correctly, having read them at a specific zoom level
35
// does not mean they are the same at the current zoom level.
36
// --
37
// This could be improved if we ever figure out how to get an event when browsers zoom,
38
// but until then we have to stick with reading client rects.
39
// --
40
// The same has been observed with Firefox on Windows7
41
// --
42
// The same has been oversved with Safari
43
return false;
44
}
45
46
return true;
47
})();
48
49
let monospaceAssumptionsAreValid = true;
50
51
export class ViewLine implements IVisibleLine {
52
53
public static readonly CLASS_NAME = 'view-line';
54
55
private _options: ViewLineOptions;
56
private _isMaybeInvalid: boolean;
57
private _renderedViewLine: IRenderedViewLine | null;
58
59
constructor(private readonly _viewGpuContext: ViewGpuContext | undefined, options: ViewLineOptions) {
60
this._options = options;
61
this._isMaybeInvalid = true;
62
this._renderedViewLine = null;
63
}
64
65
// --- begin IVisibleLineData
66
67
public getDomNode(): HTMLElement | null {
68
if (this._renderedViewLine && this._renderedViewLine.domNode) {
69
return this._renderedViewLine.domNode.domNode;
70
}
71
return null;
72
}
73
public setDomNode(domNode: HTMLElement): void {
74
if (this._renderedViewLine) {
75
this._renderedViewLine.domNode = createFastDomNode(domNode);
76
} else {
77
throw new Error('I have no rendered view line to set the dom node to...');
78
}
79
}
80
81
public onContentChanged(): void {
82
this._isMaybeInvalid = true;
83
}
84
public onTokensChanged(): void {
85
this._isMaybeInvalid = true;
86
}
87
public onDecorationsChanged(): void {
88
this._isMaybeInvalid = true;
89
}
90
public onOptionsChanged(newOptions: ViewLineOptions): void {
91
this._isMaybeInvalid = true;
92
this._options = newOptions;
93
}
94
public onSelectionChanged(): boolean {
95
if (isHighContrast(this._options.themeType) || this._renderedViewLine?.input.renderWhitespace === RenderWhitespace.Selection) {
96
this._isMaybeInvalid = true;
97
return true;
98
}
99
return false;
100
}
101
102
public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean {
103
if (this._options.useGpu && this._viewGpuContext?.canRender(this._options, viewportData, lineNumber)) {
104
this._renderedViewLine?.domNode?.domNode.remove();
105
this._renderedViewLine = null;
106
return false;
107
}
108
109
if (this._isMaybeInvalid === false) {
110
// it appears that nothing relevant has changed
111
return false;
112
}
113
114
this._isMaybeInvalid = false;
115
116
const lineData = viewportData.getViewLineRenderingData(lineNumber);
117
const options = this._options;
118
const actualInlineDecorations = LineDecoration.filter(lineData.inlineDecorations, lineNumber, lineData.minColumn, lineData.maxColumn);
119
const renderWhitespace = (lineData.hasVariableFonts || options.experimentalWhitespaceRendering === 'off') ? options.renderWhitespace : 'none';
120
const allowFastRendering = !lineData.hasVariableFonts;
121
122
// Only send selection information when needed for rendering whitespace
123
let selectionsOnLine: OffsetRange[] | null = null;
124
if (isHighContrast(options.themeType) || renderWhitespace === 'selection') {
125
const selections = viewportData.selections;
126
for (const selection of selections) {
127
128
if (selection.endLineNumber < lineNumber || selection.startLineNumber > lineNumber) {
129
// Selection does not intersect line
130
continue;
131
}
132
133
const startColumn = (selection.startLineNumber === lineNumber ? selection.startColumn : lineData.minColumn);
134
const endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn);
135
136
if (startColumn < endColumn) {
137
if (isHighContrast(options.themeType)) {
138
actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', InlineDecorationType.Regular));
139
}
140
if (renderWhitespace === 'selection') {
141
if (!selectionsOnLine) {
142
selectionsOnLine = [];
143
}
144
145
selectionsOnLine.push(new OffsetRange(startColumn - 1, endColumn - 1));
146
}
147
}
148
}
149
}
150
151
const renderLineInput = new RenderLineInput(
152
options.useMonospaceOptimizations,
153
options.canUseHalfwidthRightwardsArrow,
154
lineData.content,
155
lineData.continuesWithWrappedLine,
156
lineData.isBasicASCII,
157
lineData.containsRTL,
158
lineData.minColumn - 1,
159
lineData.tokens,
160
actualInlineDecorations,
161
lineData.tabSize,
162
lineData.startVisibleColumn,
163
options.spaceWidth,
164
options.middotWidth,
165
options.wsmiddotWidth,
166
options.stopRenderingLineAfter,
167
renderWhitespace,
168
options.renderControlCharacters,
169
options.fontLigatures !== EditorFontLigatures.OFF,
170
selectionsOnLine,
171
lineData.textDirection,
172
options.verticalScrollbarSize
173
);
174
175
if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) {
176
// no need to do anything, we have the same render input
177
return false;
178
}
179
180
sb.appendString('<div ');
181
if (lineData.textDirection === TextDirection.RTL) {
182
sb.appendString('dir="rtl" ');
183
} else if (lineData.containsRTL) {
184
sb.appendString('dir="ltr" ');
185
}
186
sb.appendString('style="top:');
187
sb.appendString(String(deltaTop));
188
sb.appendString('px;height:');
189
sb.appendString(String(lineHeight));
190
sb.appendString('px;line-height:');
191
sb.appendString(String(lineHeight));
192
if (lineData.textDirection === TextDirection.RTL) {
193
sb.appendString('px;padding-right:');
194
sb.appendString(String(options.verticalScrollbarSize));
195
}
196
sb.appendString('px;" class="');
197
sb.appendString(ViewLine.CLASS_NAME);
198
sb.appendString('">');
199
200
const output = renderViewLine(renderLineInput, sb);
201
202
sb.appendString('</div>');
203
204
let renderedViewLine: IRenderedViewLine | null = null;
205
if (
206
allowFastRendering
207
&& monospaceAssumptionsAreValid
208
&& canUseFastRenderedViewLine
209
&& lineData.isBasicASCII
210
&& renderLineInput.isLTR
211
&& options.useMonospaceOptimizations
212
&& output.containsForeignElements === ForeignElementType.None
213
) {
214
renderedViewLine = new FastRenderedViewLine(
215
this._renderedViewLine ? this._renderedViewLine.domNode : null,
216
renderLineInput,
217
output.characterMapping
218
);
219
}
220
221
if (!renderedViewLine) {
222
renderedViewLine = createRenderedLine(
223
this._renderedViewLine ? this._renderedViewLine.domNode : null,
224
renderLineInput,
225
output.characterMapping,
226
output.containsForeignElements
227
);
228
}
229
230
this._renderedViewLine = renderedViewLine;
231
232
return true;
233
}
234
235
public layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void {
236
if (this._renderedViewLine && this._renderedViewLine.domNode) {
237
this._renderedViewLine.domNode.setTop(deltaTop);
238
this._renderedViewLine.domNode.setHeight(lineHeight);
239
this._renderedViewLine.domNode.setLineHeight(lineHeight);
240
}
241
}
242
243
// --- end IVisibleLineData
244
245
public isRenderedRTL(): boolean {
246
if (!this._renderedViewLine) {
247
return false;
248
}
249
return this._renderedViewLine.input.textDirection === TextDirection.RTL;
250
}
251
252
public getWidth(context: DomReadingContext | null): number {
253
if (!this._renderedViewLine) {
254
return 0;
255
}
256
return this._renderedViewLine.getWidth(context);
257
}
258
259
public getWidthIsFast(): boolean {
260
if (!this._renderedViewLine) {
261
return true;
262
}
263
return this._renderedViewLine.getWidthIsFast();
264
}
265
266
public needsMonospaceFontCheck(): boolean {
267
if (!this._renderedViewLine) {
268
return false;
269
}
270
return (this._renderedViewLine instanceof FastRenderedViewLine);
271
}
272
273
public monospaceAssumptionsAreValid(): boolean {
274
if (!this._renderedViewLine) {
275
return monospaceAssumptionsAreValid;
276
}
277
if (this._renderedViewLine instanceof FastRenderedViewLine) {
278
return this._renderedViewLine.monospaceAssumptionsAreValid();
279
}
280
return monospaceAssumptionsAreValid;
281
}
282
283
public onMonospaceAssumptionsInvalidated(): void {
284
if (this._renderedViewLine && this._renderedViewLine instanceof FastRenderedViewLine) {
285
this._renderedViewLine = this._renderedViewLine.toSlowRenderedLine();
286
}
287
}
288
289
public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): VisibleRanges | null {
290
if (!this._renderedViewLine) {
291
return null;
292
}
293
294
startColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, startColumn));
295
endColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, endColumn));
296
297
const stopRenderingLineAfter = this._renderedViewLine.input.stopRenderingLineAfter;
298
299
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter + 1 && endColumn > stopRenderingLineAfter + 1) {
300
// This range is obviously not visible
301
return new VisibleRanges(true, [new FloatHorizontalRange(this.getWidth(context), 0)]);
302
}
303
304
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter + 1) {
305
startColumn = stopRenderingLineAfter + 1;
306
}
307
308
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter + 1) {
309
endColumn = stopRenderingLineAfter + 1;
310
}
311
312
const horizontalRanges = this._renderedViewLine.getVisibleRangesForRange(lineNumber, startColumn, endColumn, context);
313
if (horizontalRanges && horizontalRanges.length > 0) {
314
return new VisibleRanges(false, horizontalRanges);
315
}
316
317
return null;
318
}
319
320
public getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number {
321
if (!this._renderedViewLine) {
322
return 1;
323
}
324
return this._renderedViewLine.getColumnOfNodeOffset(spanNode, offset);
325
}
326
}
327
328
interface IRenderedViewLine {
329
domNode: FastDomNode<HTMLElement> | null;
330
readonly input: RenderLineInput;
331
getWidth(context: DomReadingContext | null): number;
332
getWidthIsFast(): boolean;
333
getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null;
334
getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number;
335
}
336
337
const enum Constants {
338
/**
339
* It seems that rounding errors occur with long lines, so the purely multiplication based
340
* method is only viable for short lines. For longer lines, we look up the real position of
341
* every 300th character and use multiplication based on that.
342
*
343
* See https://github.com/microsoft/vscode/issues/33178
344
*/
345
MaxMonospaceDistance = 300
346
}
347
348
/**
349
* A rendered line which is guaranteed to contain only regular ASCII and is rendered with a monospace font.
350
*/
351
class FastRenderedViewLine implements IRenderedViewLine {
352
353
public domNode: FastDomNode<HTMLElement> | null;
354
public readonly input: RenderLineInput;
355
356
private readonly _characterMapping: CharacterMapping;
357
private readonly _charWidth: number;
358
private readonly _keyColumnPixelOffsetCache: Float32Array | null;
359
private _cachedWidth: number = -1;
360
361
constructor(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping) {
362
this.domNode = domNode;
363
this.input = renderLineInput;
364
const keyColumnCount = Math.floor(renderLineInput.lineContent.length / Constants.MaxMonospaceDistance);
365
if (keyColumnCount > 0) {
366
this._keyColumnPixelOffsetCache = new Float32Array(keyColumnCount);
367
for (let i = 0; i < keyColumnCount; i++) {
368
this._keyColumnPixelOffsetCache[i] = -1;
369
}
370
} else {
371
this._keyColumnPixelOffsetCache = null;
372
}
373
374
this._characterMapping = characterMapping;
375
this._charWidth = renderLineInput.spaceWidth;
376
}
377
378
public getWidth(context: DomReadingContext | null): number {
379
if (!this.domNode || this.input.lineContent.length < Constants.MaxMonospaceDistance) {
380
const horizontalOffset = this._characterMapping.getHorizontalOffset(this._characterMapping.length);
381
return Math.round(this._charWidth * horizontalOffset);
382
}
383
if (this._cachedWidth === -1) {
384
this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth;
385
context?.markDidDomLayout();
386
}
387
return this._cachedWidth;
388
}
389
390
public getWidthIsFast(): boolean {
391
return (this.input.lineContent.length < Constants.MaxMonospaceDistance) || this._cachedWidth !== -1;
392
}
393
394
public monospaceAssumptionsAreValid(): boolean {
395
if (!this.domNode) {
396
return monospaceAssumptionsAreValid;
397
}
398
if (this.input.lineContent.length < Constants.MaxMonospaceDistance) {
399
const expectedWidth = this.getWidth(null);
400
const actualWidth = (<HTMLSpanElement>this.domNode.domNode.firstChild).offsetWidth;
401
if (Math.abs(expectedWidth - actualWidth) >= 2) {
402
// more than 2px off
403
console.warn(`monospace assumptions have been violated, therefore disabling monospace optimizations!`);
404
monospaceAssumptionsAreValid = false;
405
}
406
}
407
return monospaceAssumptionsAreValid;
408
}
409
410
public toSlowRenderedLine(): RenderedViewLine {
411
return createRenderedLine(this.domNode, this.input, this._characterMapping, ForeignElementType.None);
412
}
413
414
public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
415
const startPosition = this._getColumnPixelOffset(lineNumber, startColumn, context);
416
const endPosition = this._getColumnPixelOffset(lineNumber, endColumn, context);
417
return [new FloatHorizontalRange(startPosition, endPosition - startPosition)];
418
}
419
420
private _getColumnPixelOffset(lineNumber: number, column: number, context: DomReadingContext): number {
421
if (column <= Constants.MaxMonospaceDistance) {
422
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
423
return this._charWidth * horizontalOffset;
424
}
425
426
const keyColumnOrdinal = Math.floor((column - 1) / Constants.MaxMonospaceDistance) - 1;
427
const keyColumn = (keyColumnOrdinal + 1) * Constants.MaxMonospaceDistance + 1;
428
let keyColumnPixelOffset = -1;
429
if (this._keyColumnPixelOffsetCache) {
430
keyColumnPixelOffset = this._keyColumnPixelOffsetCache[keyColumnOrdinal];
431
if (keyColumnPixelOffset === -1) {
432
keyColumnPixelOffset = this._actualReadPixelOffset(lineNumber, keyColumn, context);
433
this._keyColumnPixelOffsetCache[keyColumnOrdinal] = keyColumnPixelOffset;
434
}
435
}
436
437
if (keyColumnPixelOffset === -1) {
438
// Could not read actual key column pixel offset
439
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
440
return this._charWidth * horizontalOffset;
441
}
442
443
const keyColumnHorizontalOffset = this._characterMapping.getHorizontalOffset(keyColumn);
444
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
445
return keyColumnPixelOffset + this._charWidth * (horizontalOffset - keyColumnHorizontalOffset);
446
}
447
448
private _getReadingTarget(myDomNode: FastDomNode<HTMLElement>): HTMLElement {
449
return <HTMLSpanElement>myDomNode.domNode.firstChild;
450
}
451
452
private _actualReadPixelOffset(lineNumber: number, column: number, context: DomReadingContext): number {
453
if (!this.domNode) {
454
return -1;
455
}
456
const domPosition = this._characterMapping.getDomPosition(column);
457
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(this.domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context);
458
if (!r || r.length === 0) {
459
return -1;
460
}
461
return r[0].left;
462
}
463
464
public getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number {
465
return getColumnOfNodeOffset(this._characterMapping, spanNode, offset);
466
}
467
}
468
469
/**
470
* Every time we render a line, we save what we have rendered in an instance of this class.
471
*/
472
class RenderedViewLine implements IRenderedViewLine {
473
474
public domNode: FastDomNode<HTMLElement> | null;
475
public readonly input: RenderLineInput;
476
477
protected readonly _characterMapping: CharacterMapping;
478
private readonly _isWhitespaceOnly: boolean;
479
private readonly _containsForeignElements: ForeignElementType;
480
private _cachedWidth: number;
481
482
/**
483
* This is a map that is used only when the line is guaranteed to be rendered LTR and has no RTL text.
484
*/
485
private readonly _pixelOffsetCache: Float32Array | null;
486
487
constructor(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) {
488
this.domNode = domNode;
489
this.input = renderLineInput;
490
this._characterMapping = characterMapping;
491
this._isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent);
492
this._containsForeignElements = containsForeignElements;
493
this._cachedWidth = -1;
494
495
this._pixelOffsetCache = null;
496
if (renderLineInput.isLTR) {
497
this._pixelOffsetCache = new Float32Array(Math.max(2, this._characterMapping.length + 1));
498
for (let column = 0, len = this._characterMapping.length; column <= len; column++) {
499
this._pixelOffsetCache[column] = -1;
500
}
501
}
502
}
503
504
// --- Reading from the DOM methods
505
506
protected _getReadingTarget(myDomNode: FastDomNode<HTMLElement>): HTMLElement {
507
return <HTMLSpanElement>myDomNode.domNode.firstChild;
508
}
509
510
/**
511
* Width of the line in pixels
512
*/
513
public getWidth(context: DomReadingContext | null): number {
514
if (!this.domNode) {
515
return 0;
516
}
517
if (this._cachedWidth === -1) {
518
this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth;
519
context?.markDidDomLayout();
520
}
521
return this._cachedWidth;
522
}
523
524
public getWidthIsFast(): boolean {
525
if (this._cachedWidth === -1) {
526
return false;
527
}
528
return true;
529
}
530
531
/**
532
* Visible ranges for a model range
533
*/
534
public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
535
if (!this.domNode) {
536
return null;
537
}
538
if (this._pixelOffsetCache !== null) {
539
// the text is guaranteed to be entirely LTR
540
const startOffset = this._readPixelOffset(this.domNode, lineNumber, startColumn, context);
541
if (startOffset === -1) {
542
return null;
543
}
544
545
const endOffset = this._readPixelOffset(this.domNode, lineNumber, endColumn, context);
546
if (endOffset === -1) {
547
return null;
548
}
549
550
return [new FloatHorizontalRange(startOffset, endOffset - startOffset)];
551
}
552
553
return this._readVisibleRangesForRange(this.domNode, lineNumber, startColumn, endColumn, context);
554
}
555
556
protected _readVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
557
if (startColumn === endColumn) {
558
const pixelOffset = this._readPixelOffset(domNode, lineNumber, startColumn, context);
559
if (pixelOffset === -1) {
560
return null;
561
} else {
562
return [new FloatHorizontalRange(pixelOffset, 0)];
563
}
564
} else {
565
return this._readRawVisibleRangesForRange(domNode, startColumn, endColumn, context);
566
}
567
}
568
569
protected _readPixelOffset(domNode: FastDomNode<HTMLElement>, lineNumber: number, column: number, context: DomReadingContext): number {
570
if (this.input.isLTR && this._characterMapping.length === 0) {
571
// This line has no content
572
if (this._containsForeignElements === ForeignElementType.None) {
573
// We can assume the line is really empty
574
return 0;
575
}
576
if (this._containsForeignElements === ForeignElementType.After) {
577
// We have foreign elements after the (empty) line
578
return 0;
579
}
580
if (this._containsForeignElements === ForeignElementType.Before) {
581
// We have foreign elements before the (empty) line
582
return this.getWidth(context);
583
}
584
// We have foreign elements before & after the (empty) line
585
const readingTarget = this._getReadingTarget(domNode);
586
if (readingTarget.firstChild) {
587
context.markDidDomLayout();
588
return (<HTMLSpanElement>readingTarget.firstChild).offsetWidth;
589
} else {
590
return 0;
591
}
592
}
593
594
if (this._pixelOffsetCache !== null) {
595
// the text is guaranteed to be LTR
596
597
const cachedPixelOffset = this._pixelOffsetCache[column];
598
if (cachedPixelOffset !== -1) {
599
return cachedPixelOffset;
600
}
601
602
const result = this._actualReadPixelOffset(domNode, lineNumber, column, context);
603
this._pixelOffsetCache[column] = result;
604
return result;
605
}
606
607
return this._actualReadPixelOffset(domNode, lineNumber, column, context);
608
}
609
610
private _actualReadPixelOffset(domNode: FastDomNode<HTMLElement>, lineNumber: number, column: number, context: DomReadingContext): number {
611
if (this._characterMapping.length === 0) {
612
// This line has no content
613
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), 0, 0, 0, 0, context);
614
if (!r || r.length === 0) {
615
return -1;
616
}
617
return r[0].left;
618
}
619
620
if (this.input.isLTR && column === this._characterMapping.length && this._isWhitespaceOnly && this._containsForeignElements === ForeignElementType.None) {
621
// This branch helps in the case of whitespace only lines which have a width set
622
return this.getWidth(context);
623
}
624
625
const domPosition = this._characterMapping.getDomPosition(column);
626
627
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context);
628
if (!r || r.length === 0) {
629
return -1;
630
}
631
const result = r[0].left;
632
if (this.input.isBasicASCII) {
633
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
634
const expectedResult = Math.round(this.input.spaceWidth * horizontalOffset);
635
if (Math.abs(expectedResult - result) <= 1) {
636
return expectedResult;
637
}
638
}
639
return result;
640
}
641
642
private _readRawVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
643
644
if (this.input.isLTR && startColumn === 1 && endColumn === this._characterMapping.length) {
645
// This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line
646
647
return [new FloatHorizontalRange(0, this.getWidth(context))];
648
}
649
650
const startDomPosition = this._characterMapping.getDomPosition(startColumn);
651
const endDomPosition = this._characterMapping.getDomPosition(endColumn);
652
653
return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startDomPosition.partIndex, startDomPosition.charIndex, endDomPosition.partIndex, endDomPosition.charIndex, context);
654
}
655
656
/**
657
* Returns the column for the text found at a specific offset inside a rendered dom node
658
*/
659
public getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number {
660
return getColumnOfNodeOffset(this._characterMapping, spanNode, offset);
661
}
662
}
663
664
class WebKitRenderedViewLine extends RenderedViewLine {
665
protected override _readVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
666
const output = super._readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context);
667
668
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) {
669
return output;
670
}
671
672
// WebKit is buggy and returns an expanded range (to contain words in some cases)
673
// The last client rect is enlarged (I think)
674
if (this.input.isLTR) {
675
// This is an attempt to patch things up
676
// Find position of last column
677
const endPixelOffset = this._readPixelOffset(domNode, lineNumber, endColumn, context);
678
if (endPixelOffset !== -1) {
679
const lastRange = output[output.length - 1];
680
if (lastRange.left < endPixelOffset) {
681
// Trim down the width of the last visible range to not go after the last column's position
682
lastRange.width = endPixelOffset - lastRange.left;
683
}
684
}
685
}
686
687
return output;
688
}
689
}
690
691
const createRenderedLine: (domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) => RenderedViewLine = (function () {
692
if (browser.isWebKit) {
693
return createWebKitRenderedLine;
694
}
695
return createNormalRenderedLine;
696
})();
697
698
function createWebKitRenderedLine(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType): RenderedViewLine {
699
return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsForeignElements);
700
}
701
702
function createNormalRenderedLine(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType): RenderedViewLine {
703
return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsForeignElements);
704
}
705
706
export function getColumnOfNodeOffset(characterMapping: CharacterMapping, spanNode: HTMLElement, offset: number): number {
707
const spanNodeTextContentLength = spanNode.textContent!.length;
708
709
let spanIndex = -1;
710
while (spanNode) {
711
spanNode = <HTMLElement>spanNode.previousSibling;
712
spanIndex++;
713
}
714
715
return characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength);
716
}
717
718