Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/viewLines/rangeUtil.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 { Constants } from '../../../../base/common/uint.js';
7
import { FloatHorizontalRange } from '../../view/renderingContext.js';
8
import { DomReadingContext } from './domReadingContext.js';
9
10
export class RangeUtil {
11
12
/**
13
* Reusing the same range here
14
* because IE is buggy and constantly freezes when using a large number
15
* of ranges and calling .detach on them
16
*/
17
private static _handyReadyRange: Range;
18
19
private static _createRange(): Range {
20
if (!this._handyReadyRange) {
21
this._handyReadyRange = document.createRange();
22
}
23
return this._handyReadyRange;
24
}
25
26
private static _detachRange(range: Range, endNode: HTMLElement): void {
27
// Move range out of the span node, IE doesn't like having many ranges in
28
// the same spot and will act badly for lines containing dashes ('-')
29
range.selectNodeContents(endNode);
30
}
31
32
private static _readClientRects(startElement: Node, startOffset: number, endElement: Node, endOffset: number, endNode: HTMLElement): DOMRectList | null {
33
const range = this._createRange();
34
try {
35
range.setStart(startElement, startOffset);
36
range.setEnd(endElement, endOffset);
37
38
return range.getClientRects();
39
} catch (e) {
40
// This is life ...
41
return null;
42
} finally {
43
this._detachRange(range, endNode);
44
}
45
}
46
47
private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): FloatHorizontalRange[] {
48
if (ranges.length === 1) {
49
// There is nothing to merge
50
return ranges;
51
}
52
53
ranges.sort(FloatHorizontalRange.compare);
54
55
const result: FloatHorizontalRange[] = [];
56
let resultLen = 0;
57
let prev = ranges[0];
58
59
for (let i = 1, len = ranges.length; i < len; i++) {
60
const range = ranges[i];
61
if (prev.left + prev.width + 0.9 /* account for browser's rounding errors*/ >= range.left) {
62
prev.width = Math.max(prev.width, range.left + range.width - prev.left);
63
} else {
64
result[resultLen++] = prev;
65
prev = range;
66
}
67
}
68
69
result[resultLen++] = prev;
70
71
return result;
72
}
73
74
private static _createHorizontalRangesFromClientRects(clientRects: DOMRectList | null, clientRectDeltaLeft: number, clientRectScale: number): FloatHorizontalRange[] | null {
75
if (!clientRects || clientRects.length === 0) {
76
return null;
77
}
78
79
// We go through FloatHorizontalRange because it has been observed in bi-di text
80
// that the clientRects are not coming in sorted from the browser
81
82
const result: FloatHorizontalRange[] = [];
83
for (let i = 0, len = clientRects.length; i < len; i++) {
84
const clientRect = clientRects[i];
85
result[i] = new FloatHorizontalRange(Math.max(0, (clientRect.left - clientRectDeltaLeft) / clientRectScale), clientRect.width / clientRectScale);
86
}
87
88
return this._mergeAdjacentRanges(result);
89
}
90
91
public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, context: DomReadingContext): FloatHorizontalRange[] | null {
92
// Panic check
93
const min = 0;
94
const max = domNode.children.length - 1;
95
if (min > max) {
96
return null;
97
}
98
startChildIndex = Math.min(max, Math.max(min, startChildIndex));
99
endChildIndex = Math.min(max, Math.max(min, endChildIndex));
100
101
if (startChildIndex === endChildIndex && startOffset === endOffset && startOffset === 0 && !domNode.children[startChildIndex].firstChild) {
102
// We must find the position at the beginning of a <span>
103
// To cover cases of empty <span>s, avoid using a range and use the <span>'s bounding box
104
const clientRects = domNode.children[startChildIndex].getClientRects();
105
context.markDidDomLayout();
106
return this._createHorizontalRangesFromClientRects(clientRects, context.clientRectDeltaLeft, context.clientRectScale);
107
}
108
109
// If crossing over to a span only to select offset 0, then use the previous span's maximum offset
110
// Chrome is buggy and doesn't handle 0 offsets well sometimes.
111
if (startChildIndex !== endChildIndex) {
112
if (endChildIndex > 0 && endOffset === 0) {
113
endChildIndex--;
114
endOffset = Constants.MAX_SAFE_SMALL_INTEGER;
115
}
116
}
117
118
let startElement = domNode.children[startChildIndex].firstChild;
119
let endElement = domNode.children[endChildIndex].firstChild;
120
121
if (!startElement || !endElement) {
122
// When having an empty <span> (without any text content), try to move to the previous <span>
123
if (!startElement && startOffset === 0 && startChildIndex > 0) {
124
startElement = domNode.children[startChildIndex - 1].firstChild;
125
startOffset = Constants.MAX_SAFE_SMALL_INTEGER;
126
}
127
if (!endElement && endOffset === 0 && endChildIndex > 0) {
128
endElement = domNode.children[endChildIndex - 1].firstChild;
129
endOffset = Constants.MAX_SAFE_SMALL_INTEGER;
130
}
131
}
132
133
if (!startElement || !endElement) {
134
return null;
135
}
136
137
startOffset = Math.min(startElement.textContent!.length, Math.max(0, startOffset));
138
endOffset = Math.min(endElement.textContent!.length, Math.max(0, endOffset));
139
140
const clientRects = this._readClientRects(startElement, startOffset, endElement, endOffset, context.endNode);
141
context.markDidDomLayout();
142
return this._createHorizontalRangesFromClientRects(clientRects, context.clientRectDeltaLeft, context.clientRectScale);
143
}
144
}
145
146