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
5251 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
public resetCachedWidth(): void {
328
this._renderedViewLine?.resetCachedWidth();
329
}
330
}
331
332
interface IRenderedViewLine {
333
domNode: FastDomNode<HTMLElement> | null;
334
readonly input: RenderLineInput;
335
getWidth(context: DomReadingContext | null): number;
336
getWidthIsFast(): boolean;
337
resetCachedWidth(): void;
338
getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null;
339
getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number;
340
}
341
342
const enum Constants {
343
/**
344
* It seems that rounding errors occur with long lines, so the purely multiplication based
345
* method is only viable for short lines. For longer lines, we look up the real position of
346
* every 300th character and use multiplication based on that.
347
*
348
* See https://github.com/microsoft/vscode/issues/33178
349
*/
350
MaxMonospaceDistance = 300
351
}
352
353
/**
354
* A rendered line which is guaranteed to contain only regular ASCII and is rendered with a monospace font.
355
*/
356
class FastRenderedViewLine implements IRenderedViewLine {
357
358
public domNode: FastDomNode<HTMLElement> | null;
359
public readonly input: RenderLineInput;
360
361
private readonly _characterMapping: CharacterMapping;
362
private readonly _charWidth: number;
363
private readonly _keyColumnPixelOffsetCache: Float32Array | null;
364
private _cachedWidth: number = -1;
365
366
constructor(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping) {
367
this.domNode = domNode;
368
this.input = renderLineInput;
369
const keyColumnCount = Math.floor(renderLineInput.lineContent.length / Constants.MaxMonospaceDistance);
370
if (keyColumnCount > 0) {
371
this._keyColumnPixelOffsetCache = new Float32Array(keyColumnCount);
372
for (let i = 0; i < keyColumnCount; i++) {
373
this._keyColumnPixelOffsetCache[i] = -1;
374
}
375
} else {
376
this._keyColumnPixelOffsetCache = null;
377
}
378
379
this._characterMapping = characterMapping;
380
this._charWidth = renderLineInput.spaceWidth;
381
}
382
383
public getWidth(context: DomReadingContext | null): number {
384
if (!this.domNode || this.input.lineContent.length < Constants.MaxMonospaceDistance) {
385
const horizontalOffset = this._characterMapping.getHorizontalOffset(this._characterMapping.length);
386
return Math.round(this._charWidth * horizontalOffset);
387
}
388
if (this._cachedWidth === -1) {
389
this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth;
390
context?.markDidDomLayout();
391
}
392
return this._cachedWidth;
393
}
394
395
public getWidthIsFast(): boolean {
396
return (this.input.lineContent.length < Constants.MaxMonospaceDistance) || this._cachedWidth !== -1;
397
}
398
399
public resetCachedWidth(): void {
400
this._cachedWidth = -1;
401
}
402
403
public monospaceAssumptionsAreValid(): boolean {
404
if (!this.domNode) {
405
return monospaceAssumptionsAreValid;
406
}
407
if (this.input.lineContent.length < Constants.MaxMonospaceDistance) {
408
const expectedWidth = this.getWidth(null);
409
const actualWidth = (<HTMLSpanElement>this.domNode.domNode.firstChild).offsetWidth;
410
if (Math.abs(expectedWidth - actualWidth) >= 2) {
411
// more than 2px off
412
console.warn(`monospace assumptions have been violated, therefore disabling monospace optimizations!`);
413
monospaceAssumptionsAreValid = false;
414
}
415
}
416
return monospaceAssumptionsAreValid;
417
}
418
419
public toSlowRenderedLine(): RenderedViewLine {
420
return createRenderedLine(this.domNode, this.input, this._characterMapping, ForeignElementType.None);
421
}
422
423
public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
424
const startPosition = this._getColumnPixelOffset(lineNumber, startColumn, context);
425
const endPosition = this._getColumnPixelOffset(lineNumber, endColumn, context);
426
return [new FloatHorizontalRange(startPosition, endPosition - startPosition)];
427
}
428
429
private _getColumnPixelOffset(lineNumber: number, column: number, context: DomReadingContext): number {
430
if (column <= Constants.MaxMonospaceDistance) {
431
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
432
return this._charWidth * horizontalOffset;
433
}
434
435
const keyColumnOrdinal = Math.floor((column - 1) / Constants.MaxMonospaceDistance) - 1;
436
const keyColumn = (keyColumnOrdinal + 1) * Constants.MaxMonospaceDistance + 1;
437
let keyColumnPixelOffset = -1;
438
if (this._keyColumnPixelOffsetCache) {
439
keyColumnPixelOffset = this._keyColumnPixelOffsetCache[keyColumnOrdinal];
440
if (keyColumnPixelOffset === -1) {
441
keyColumnPixelOffset = this._actualReadPixelOffset(lineNumber, keyColumn, context);
442
this._keyColumnPixelOffsetCache[keyColumnOrdinal] = keyColumnPixelOffset;
443
}
444
}
445
446
if (keyColumnPixelOffset === -1) {
447
// Could not read actual key column pixel offset
448
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
449
return this._charWidth * horizontalOffset;
450
}
451
452
const keyColumnHorizontalOffset = this._characterMapping.getHorizontalOffset(keyColumn);
453
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
454
return keyColumnPixelOffset + this._charWidth * (horizontalOffset - keyColumnHorizontalOffset);
455
}
456
457
private _getReadingTarget(myDomNode: FastDomNode<HTMLElement>): HTMLElement {
458
return <HTMLSpanElement>myDomNode.domNode.firstChild;
459
}
460
461
private _actualReadPixelOffset(lineNumber: number, column: number, context: DomReadingContext): number {
462
if (!this.domNode) {
463
return -1;
464
}
465
const domPosition = this._characterMapping.getDomPosition(column);
466
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(this.domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context);
467
if (!r || r.length === 0) {
468
return -1;
469
}
470
return r[0].left;
471
}
472
473
public getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number {
474
return getColumnOfNodeOffset(this._characterMapping, spanNode, offset);
475
}
476
}
477
478
/**
479
* Every time we render a line, we save what we have rendered in an instance of this class.
480
*/
481
class RenderedViewLine implements IRenderedViewLine {
482
483
public domNode: FastDomNode<HTMLElement> | null;
484
public readonly input: RenderLineInput;
485
486
protected readonly _characterMapping: CharacterMapping;
487
private readonly _isWhitespaceOnly: boolean;
488
private readonly _containsForeignElements: ForeignElementType;
489
private _cachedWidth: number;
490
491
/**
492
* This is a map that is used only when the line is guaranteed to be rendered LTR and has no RTL text.
493
*/
494
private readonly _pixelOffsetCache: Float32Array | null;
495
496
constructor(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) {
497
this.domNode = domNode;
498
this.input = renderLineInput;
499
this._characterMapping = characterMapping;
500
this._isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent);
501
this._containsForeignElements = containsForeignElements;
502
this._cachedWidth = -1;
503
504
this._pixelOffsetCache = null;
505
if (renderLineInput.isLTR) {
506
this._pixelOffsetCache = new Float32Array(Math.max(2, this._characterMapping.length + 1));
507
for (let column = 0, len = this._characterMapping.length; column <= len; column++) {
508
this._pixelOffsetCache[column] = -1;
509
}
510
}
511
}
512
513
// --- Reading from the DOM methods
514
515
protected _getReadingTarget(myDomNode: FastDomNode<HTMLElement>): HTMLElement {
516
return <HTMLSpanElement>myDomNode.domNode.firstChild;
517
}
518
519
/**
520
* Width of the line in pixels
521
*/
522
public getWidth(context: DomReadingContext | null): number {
523
if (!this.domNode) {
524
return 0;
525
}
526
if (this._cachedWidth === -1) {
527
this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth;
528
context?.markDidDomLayout();
529
}
530
return this._cachedWidth;
531
}
532
533
public getWidthIsFast(): boolean {
534
if (this._cachedWidth === -1) {
535
return false;
536
}
537
return true;
538
}
539
540
public resetCachedWidth(): void {
541
this._cachedWidth = -1;
542
if (this._pixelOffsetCache !== null) {
543
for (let column = 0, len = this._pixelOffsetCache.length; column < len; column++) {
544
this._pixelOffsetCache[column] = -1;
545
}
546
}
547
}
548
549
/**
550
* Visible ranges for a model range
551
*/
552
public getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
553
if (!this.domNode) {
554
return null;
555
}
556
if (this._pixelOffsetCache !== null) {
557
// the text is guaranteed to be entirely LTR
558
const startOffset = this._readPixelOffset(this.domNode, lineNumber, startColumn, context);
559
if (startOffset === -1) {
560
return null;
561
}
562
563
const endOffset = this._readPixelOffset(this.domNode, lineNumber, endColumn, context);
564
if (endOffset === -1) {
565
return null;
566
}
567
568
return [new FloatHorizontalRange(startOffset, endOffset - startOffset)];
569
}
570
571
return this._readVisibleRangesForRange(this.domNode, lineNumber, startColumn, endColumn, context);
572
}
573
574
protected _readVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
575
if (startColumn === endColumn) {
576
const pixelOffset = this._readPixelOffset(domNode, lineNumber, startColumn, context);
577
if (pixelOffset === -1) {
578
return null;
579
} else {
580
return [new FloatHorizontalRange(pixelOffset, 0)];
581
}
582
} else {
583
return this._readRawVisibleRangesForRange(domNode, startColumn, endColumn, context);
584
}
585
}
586
587
protected _readPixelOffset(domNode: FastDomNode<HTMLElement>, lineNumber: number, column: number, context: DomReadingContext): number {
588
if (this.input.isLTR && this._characterMapping.length === 0) {
589
// This line has no content
590
if (this._containsForeignElements === ForeignElementType.None) {
591
// We can assume the line is really empty
592
return 0;
593
}
594
if (this._containsForeignElements === ForeignElementType.After) {
595
// We have foreign elements after the (empty) line
596
return 0;
597
}
598
if (this._containsForeignElements === ForeignElementType.Before) {
599
// We have foreign elements before the (empty) line
600
return this.getWidth(context);
601
}
602
// We have foreign elements before & after the (empty) line
603
const readingTarget = this._getReadingTarget(domNode);
604
if (readingTarget.firstChild) {
605
context.markDidDomLayout();
606
return (<HTMLSpanElement>readingTarget.firstChild).offsetWidth;
607
} else {
608
return 0;
609
}
610
}
611
612
if (this._pixelOffsetCache !== null) {
613
// the text is guaranteed to be LTR
614
615
const cachedPixelOffset = this._pixelOffsetCache[column];
616
if (cachedPixelOffset !== -1) {
617
return cachedPixelOffset;
618
}
619
620
const result = this._actualReadPixelOffset(domNode, lineNumber, column, context);
621
this._pixelOffsetCache[column] = result;
622
return result;
623
}
624
625
return this._actualReadPixelOffset(domNode, lineNumber, column, context);
626
}
627
628
private _actualReadPixelOffset(domNode: FastDomNode<HTMLElement>, lineNumber: number, column: number, context: DomReadingContext): number {
629
if (this._characterMapping.length === 0) {
630
// This line has no content
631
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), 0, 0, 0, 0, context);
632
if (!r || r.length === 0) {
633
return -1;
634
}
635
return r[0].left;
636
}
637
638
if (this.input.isLTR && column === this._characterMapping.length && this._isWhitespaceOnly && this._containsForeignElements === ForeignElementType.None) {
639
// This branch helps in the case of whitespace only lines which have a width set
640
return this.getWidth(context);
641
}
642
643
const domPosition = this._characterMapping.getDomPosition(column);
644
645
const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), domPosition.partIndex, domPosition.charIndex, domPosition.partIndex, domPosition.charIndex, context);
646
if (!r || r.length === 0) {
647
return -1;
648
}
649
const result = r[0].left;
650
if (this.input.isBasicASCII) {
651
const horizontalOffset = this._characterMapping.getHorizontalOffset(column);
652
const expectedResult = Math.round(this.input.spaceWidth * horizontalOffset);
653
if (Math.abs(expectedResult - result) <= 1) {
654
return expectedResult;
655
}
656
}
657
return result;
658
}
659
660
private _readRawVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
661
662
if (this.input.isLTR && startColumn === 1 && endColumn === this._characterMapping.length) {
663
// This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line
664
665
return [new FloatHorizontalRange(0, this.getWidth(context))];
666
}
667
668
const startDomPosition = this._characterMapping.getDomPosition(startColumn);
669
const endDomPosition = this._characterMapping.getDomPosition(endColumn);
670
671
return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startDomPosition.partIndex, startDomPosition.charIndex, endDomPosition.partIndex, endDomPosition.charIndex, context);
672
}
673
674
/**
675
* Returns the column for the text found at a specific offset inside a rendered dom node
676
*/
677
public getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number {
678
return getColumnOfNodeOffset(this._characterMapping, spanNode, offset);
679
}
680
}
681
682
class WebKitRenderedViewLine extends RenderedViewLine {
683
protected override _readVisibleRangesForRange(domNode: FastDomNode<HTMLElement>, lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null {
684
const output = super._readVisibleRangesForRange(domNode, lineNumber, startColumn, endColumn, context);
685
686
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) {
687
return output;
688
}
689
690
// WebKit is buggy and returns an expanded range (to contain words in some cases)
691
// The last client rect is enlarged (I think)
692
if (this.input.isLTR) {
693
// This is an attempt to patch things up
694
// Find position of last column
695
const endPixelOffset = this._readPixelOffset(domNode, lineNumber, endColumn, context);
696
if (endPixelOffset !== -1) {
697
const lastRange = output[output.length - 1];
698
if (lastRange.left < endPixelOffset) {
699
// Trim down the width of the last visible range to not go after the last column's position
700
lastRange.width = endPixelOffset - lastRange.left;
701
}
702
}
703
}
704
705
return output;
706
}
707
}
708
709
const createRenderedLine: (domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) => RenderedViewLine = (function () {
710
if (browser.isWebKit) {
711
return createWebKitRenderedLine;
712
}
713
return createNormalRenderedLine;
714
})();
715
716
function createWebKitRenderedLine(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType): RenderedViewLine {
717
return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsForeignElements);
718
}
719
720
function createNormalRenderedLine(domNode: FastDomNode<HTMLElement> | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsForeignElements: ForeignElementType): RenderedViewLine {
721
return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsForeignElements);
722
}
723
724
export function getColumnOfNodeOffset(characterMapping: CharacterMapping, spanNode: HTMLElement, offset: number): number {
725
const spanNodeTextContentLength = spanNode.textContent.length;
726
727
let spanIndex = -1;
728
while (spanNode) {
729
spanNode = <HTMLElement>spanNode.previousSibling;
730
spanIndex++;
731
}
732
733
return characterMapping.getColumn(new DomPosition(spanIndex, offset), spanNodeTextContentLength);
734
}
735
736