@blankdotpage/cake 0.1.68 → 0.1.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cake/core/mapping/cursor-source-map.d.ts +11 -0
- package/dist/cake/core/mapping/cursor-source-map.d.ts.map +1 -1
- package/dist/cake/core/mapping/cursor-source-map.js +159 -21
- package/dist/cake/core/runtime.d.ts +4 -0
- package/dist/cake/core/runtime.d.ts.map +1 -1
- package/dist/cake/core/runtime.js +332 -215
- package/dist/cake/dom/render.d.ts +32 -2
- package/dist/cake/dom/render.d.ts.map +1 -1
- package/dist/cake/dom/render.js +401 -118
- package/dist/cake/editor/cake-editor.d.ts +8 -1
- package/dist/cake/editor/cake-editor.d.ts.map +1 -1
- package/dist/cake/editor/cake-editor.js +172 -100
- package/dist/cake/editor/internal/editor-text-model.d.ts +49 -0
- package/dist/cake/editor/internal/editor-text-model.d.ts.map +1 -0
- package/dist/cake/editor/internal/editor-text-model.js +284 -0
- package/dist/cake/editor/selection/selection-geometry-dom.d.ts +5 -1
- package/dist/cake/editor/selection/selection-geometry-dom.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-geometry-dom.js +4 -5
- package/dist/cake/editor/selection/selection-layout-dom.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-layout-dom.js +2 -5
- package/dist/cake/editor/selection/selection-layout.d.ts +2 -15
- package/dist/cake/editor/selection/selection-layout.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-layout.js +1 -99
- package/dist/cake/editor/selection/selection-navigation.d.ts +4 -0
- package/dist/cake/editor/selection/selection-navigation.d.ts.map +1 -1
- package/dist/cake/editor/selection/selection-navigation.js +1 -2
- package/dist/cake/extensions/link/link.d.ts.map +1 -1
- package/dist/cake/extensions/link/link.js +1 -7
- package/dist/cake/extensions/shared/structural-reparse-policy.js +2 -2
- package/package.json +5 -2
- package/dist/cake/editor/selection/visible-text.d.ts +0 -5
- package/dist/cake/editor/selection/visible-text.d.ts.map +0 -1
- package/dist/cake/editor/selection/visible-text.js +0 -66
- package/dist/cake/engine/cake-engine.d.ts +0 -230
- package/dist/cake/engine/cake-engine.d.ts.map +0 -1
- package/dist/cake/engine/cake-engine.js +0 -3589
- package/dist/cake/engine/selection/selection-geometry-dom.d.ts +0 -24
- package/dist/cake/engine/selection/selection-geometry-dom.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-geometry-dom.js +0 -302
- package/dist/cake/engine/selection/selection-geometry.d.ts +0 -22
- package/dist/cake/engine/selection/selection-geometry.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-geometry.js +0 -158
- package/dist/cake/engine/selection/selection-layout-dom.d.ts +0 -50
- package/dist/cake/engine/selection/selection-layout-dom.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-layout-dom.js +0 -781
- package/dist/cake/engine/selection/selection-layout.d.ts +0 -55
- package/dist/cake/engine/selection/selection-layout.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-layout.js +0 -128
- package/dist/cake/engine/selection/selection-navigation.d.ts +0 -22
- package/dist/cake/engine/selection/selection-navigation.d.ts.map +0 -1
- package/dist/cake/engine/selection/selection-navigation.js +0 -229
- package/dist/cake/engine/selection/visible-text.d.ts +0 -5
- package/dist/cake/engine/selection/visible-text.d.ts.map +0 -1
- package/dist/cake/engine/selection/visible-text.js +0 -66
- package/dist/cake/react/CakeEditor.d.ts +0 -58
- package/dist/cake/react/CakeEditor.d.ts.map +0 -1
- package/dist/cake/react/CakeEditor.js +0 -225
|
@@ -1,781 +0,0 @@
|
|
|
1
|
-
import { buildLayoutModel, getLineOffsets } from "./selection-layout";
|
|
2
|
-
export function toLayoutRect(params) {
|
|
3
|
-
return {
|
|
4
|
-
top: params.rect.top - params.containerRect.top + params.scroll.top,
|
|
5
|
-
left: params.rect.left - params.containerRect.left + params.scroll.left,
|
|
6
|
-
width: params.rect.width,
|
|
7
|
-
height: params.rect.height,
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
function mergeDomRects(rects) {
|
|
11
|
-
if (rects.length === 0) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
let left = rects[0]?.left ?? 0;
|
|
15
|
-
let top = rects[0]?.top ?? 0;
|
|
16
|
-
let right = left + (rects[0]?.width ?? 0);
|
|
17
|
-
let bottom = top + (rects[0]?.height ?? 0);
|
|
18
|
-
rects.forEach((rect) => {
|
|
19
|
-
const rectRight = rect.left + rect.width;
|
|
20
|
-
const rectBottom = rect.top + rect.height;
|
|
21
|
-
left = Math.min(left, rect.left);
|
|
22
|
-
top = Math.min(top, rect.top);
|
|
23
|
-
right = Math.max(right, rectRight);
|
|
24
|
-
bottom = Math.max(bottom, rectBottom);
|
|
25
|
-
});
|
|
26
|
-
return new DOMRect(left, top, right - left, bottom - top);
|
|
27
|
-
}
|
|
28
|
-
export function groupDomRectsByRow(rects) {
|
|
29
|
-
if (rects.length === 0) {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
// IMPORTANT:
|
|
33
|
-
// `Range.getClientRects()` returns one rect per line box fragment, but in some
|
|
34
|
-
// engines (and/or with small line-heights) adjacent fragments can *overlap*
|
|
35
|
-
// vertically. Grouping-by-overlap will incorrectly merge multiple rows into one.
|
|
36
|
-
//
|
|
37
|
-
// Instead, group by (approximately) equal `top` and merge only within that row.
|
|
38
|
-
const ROW_TOP_EPS_PX = 1;
|
|
39
|
-
const sorted = [...rects].sort((a, b) => a.top === b.top ? a.left - b.left : a.top - b.top);
|
|
40
|
-
const grouped = [];
|
|
41
|
-
for (const rect of sorted) {
|
|
42
|
-
const last = grouped[grouped.length - 1];
|
|
43
|
-
if (last && Math.abs(rect.top - last.top) <= ROW_TOP_EPS_PX) {
|
|
44
|
-
grouped[grouped.length - 1] = mergeDomRects([last, rect]) ?? last;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
grouped.push(rect);
|
|
48
|
-
}
|
|
49
|
-
return grouped;
|
|
50
|
-
}
|
|
51
|
-
function cursorOffsetToCodeUnit(cursorToCodeUnit, offset) {
|
|
52
|
-
if (cursorToCodeUnit.length === 0) {
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
const clamped = Math.max(0, Math.min(offset, cursorToCodeUnit.length - 1));
|
|
56
|
-
return cursorToCodeUnit[clamped] ?? 0;
|
|
57
|
-
}
|
|
58
|
-
function createDomPositionResolver(lineElement) {
|
|
59
|
-
const textNodes = [];
|
|
60
|
-
const cumulativeEnds = [];
|
|
61
|
-
const walker = createTextWalker(lineElement);
|
|
62
|
-
let current = walker.nextNode();
|
|
63
|
-
let total = 0;
|
|
64
|
-
while (current) {
|
|
65
|
-
if (current instanceof Text) {
|
|
66
|
-
const length = current.data.length;
|
|
67
|
-
textNodes.push(current);
|
|
68
|
-
total += length;
|
|
69
|
-
cumulativeEnds.push(total);
|
|
70
|
-
}
|
|
71
|
-
current = walker.nextNode();
|
|
72
|
-
}
|
|
73
|
-
if (textNodes.length === 0) {
|
|
74
|
-
return () => {
|
|
75
|
-
if (!lineElement.textContent) {
|
|
76
|
-
return { node: lineElement, offset: 0 };
|
|
77
|
-
}
|
|
78
|
-
return { node: lineElement, offset: lineElement.childNodes.length };
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return (offsetInLine) => {
|
|
82
|
-
const clamped = Math.max(0, Math.min(offsetInLine, total));
|
|
83
|
-
let low = 0;
|
|
84
|
-
let high = cumulativeEnds.length - 1;
|
|
85
|
-
while (low < high) {
|
|
86
|
-
const mid = low + high >>> 1;
|
|
87
|
-
if ((cumulativeEnds[mid] ?? 0) < clamped) {
|
|
88
|
-
low = mid + 1;
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
high = mid;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const node = textNodes[low] ?? lineElement;
|
|
95
|
-
const prevEnd = low > 0 ? (cumulativeEnds[low - 1] ?? 0) : 0;
|
|
96
|
-
return { node, offset: clamped - prevEnd };
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function measureCharacterRect(params) {
|
|
100
|
-
if (params.lineLength <= 0) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const startCodeUnit = cursorOffsetToCodeUnit(params.cursorToCodeUnit, params.offset);
|
|
104
|
-
const endCodeUnit = cursorOffsetToCodeUnit(params.cursorToCodeUnit, Math.min(params.offset + 1, params.lineLength));
|
|
105
|
-
const startPosition = params.resolveDomPosition(startCodeUnit);
|
|
106
|
-
const endPosition = params.resolveDomPosition(endCodeUnit);
|
|
107
|
-
// If start and end are in different text nodes and end is at offset 0 of next node,
|
|
108
|
-
// try measuring within the end node instead (the character lives there)
|
|
109
|
-
if (startPosition.node !== endPosition.node &&
|
|
110
|
-
startPosition.node instanceof Text &&
|
|
111
|
-
endPosition.node instanceof Text &&
|
|
112
|
-
startPosition.offset === startPosition.node.length &&
|
|
113
|
-
endPosition.offset > 0) {
|
|
114
|
-
// The character is in the end node, measure from offset 0 to endPosition.offset
|
|
115
|
-
params.range.setStart(endPosition.node, 0);
|
|
116
|
-
params.range.setEnd(endPosition.node, endPosition.offset);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
params.range.setStart(startPosition.node, startPosition.offset);
|
|
120
|
-
params.range.setEnd(endPosition.node, endPosition.offset);
|
|
121
|
-
}
|
|
122
|
-
const rects = params.range.getClientRects();
|
|
123
|
-
if (rects.length > 0) {
|
|
124
|
-
// Some engines can include zero-width fragments for a single character
|
|
125
|
-
// (notably at soft wrap boundaries). Prefer the largest rect.
|
|
126
|
-
const list = Array.from(rects);
|
|
127
|
-
let best = list[0] ?? null;
|
|
128
|
-
let bestArea = best ? best.width * best.height : 0;
|
|
129
|
-
for (const rect of list) {
|
|
130
|
-
const area = rect.width * rect.height;
|
|
131
|
-
if (area > bestArea) {
|
|
132
|
-
best = rect;
|
|
133
|
-
bestArea = area;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return bestArea > 0 ? best : (list[0] ?? null);
|
|
137
|
-
}
|
|
138
|
-
const rect = params.range.getBoundingClientRect();
|
|
139
|
-
if (rect.width === 0 && rect.height === 0) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
return rect;
|
|
143
|
-
}
|
|
144
|
-
function measureLineRows(params) {
|
|
145
|
-
const fallbackLineBox = toLayoutRect({
|
|
146
|
-
rect: params.lineRect,
|
|
147
|
-
containerRect: params.containerRect,
|
|
148
|
-
scroll: params.scroll,
|
|
149
|
-
});
|
|
150
|
-
if (params.lineLength === 0) {
|
|
151
|
-
return [
|
|
152
|
-
{
|
|
153
|
-
startOffset: 0,
|
|
154
|
-
endOffset: 0,
|
|
155
|
-
rect: { ...fallbackLineBox, width: 0 },
|
|
156
|
-
},
|
|
157
|
-
];
|
|
158
|
-
}
|
|
159
|
-
const WRAP_THRESHOLD_PX = 3;
|
|
160
|
-
const resolvePosition = createDomPositionResolver(params.lineElement);
|
|
161
|
-
const scratchRange = document.createRange();
|
|
162
|
-
const topCache = new Map();
|
|
163
|
-
const fullLineStart = resolvePosition(0);
|
|
164
|
-
const fullLineEnd = resolvePosition(params.codeUnitLength);
|
|
165
|
-
scratchRange.setStart(fullLineStart.node, fullLineStart.offset);
|
|
166
|
-
scratchRange.setEnd(fullLineEnd.node, fullLineEnd.offset);
|
|
167
|
-
const fullLineRects = groupDomRectsByRow(Array.from(scratchRange.getClientRects()));
|
|
168
|
-
if (fullLineRects.length === 0) {
|
|
169
|
-
return [
|
|
170
|
-
{
|
|
171
|
-
startOffset: 0,
|
|
172
|
-
endOffset: params.lineLength,
|
|
173
|
-
rect: fallbackLineBox,
|
|
174
|
-
},
|
|
175
|
-
];
|
|
176
|
-
}
|
|
177
|
-
// Convert fragment rects into non-overlapping row boxes by clamping each row's
|
|
178
|
-
// height to the distance to the next row top. This avoids downstream logic
|
|
179
|
-
// (hit-testing, center-based row selection, etc.) being affected by engines
|
|
180
|
-
// that report overlapping line box heights.
|
|
181
|
-
const rowRects = fullLineRects.map((rect, index) => {
|
|
182
|
-
const nextTop = fullLineRects[index + 1]?.top ?? params.lineRect.bottom;
|
|
183
|
-
const bottom = Math.max(rect.top, nextTop);
|
|
184
|
-
const height = Math.max(0, bottom - rect.top);
|
|
185
|
-
return new DOMRect(rect.left, rect.top, rect.width, height);
|
|
186
|
-
});
|
|
187
|
-
function offsetToTop(offset) {
|
|
188
|
-
if (topCache.has(offset)) {
|
|
189
|
-
return topCache.get(offset) ?? null;
|
|
190
|
-
}
|
|
191
|
-
const rect = measureCharacterRect({
|
|
192
|
-
lineElement: params.lineElement,
|
|
193
|
-
offset,
|
|
194
|
-
lineLength: params.lineLength,
|
|
195
|
-
cursorToCodeUnit: params.cursorToCodeUnit,
|
|
196
|
-
resolveDomPosition: resolvePosition,
|
|
197
|
-
range: scratchRange,
|
|
198
|
-
});
|
|
199
|
-
let top = rect ? rect.top : null;
|
|
200
|
-
if (top === null) {
|
|
201
|
-
// Some engines occasionally fail to return a usable rect for certain
|
|
202
|
-
// characters (notably at soft-wrap boundaries). Fall back to measuring the
|
|
203
|
-
// caret position at this offset so row detection remains stable.
|
|
204
|
-
const codeUnitOffset = cursorOffsetToCodeUnit(params.cursorToCodeUnit, offset);
|
|
205
|
-
const position = resolvePosition(codeUnitOffset);
|
|
206
|
-
scratchRange.setStart(position.node, position.offset);
|
|
207
|
-
scratchRange.setEnd(position.node, position.offset);
|
|
208
|
-
const rects = scratchRange.getClientRects();
|
|
209
|
-
if (rects.length > 0) {
|
|
210
|
-
top = rects[0]?.top ?? null;
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
const caretRect = scratchRange.getBoundingClientRect();
|
|
214
|
-
if (!(caretRect.width === 0 && caretRect.height === 0)) {
|
|
215
|
-
top = caretRect.top;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
topCache.set(offset, top);
|
|
220
|
-
return top;
|
|
221
|
-
}
|
|
222
|
-
function findFirstMeasurableOffset(from) {
|
|
223
|
-
for (let offset = Math.max(0, from); offset < params.lineLength; offset++) {
|
|
224
|
-
if (offsetToTop(offset) !== null) {
|
|
225
|
-
return offset;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
function findNextRowStartOffset(fromExclusive, rowTop) {
|
|
231
|
-
const lastIndex = params.lineLength - 1;
|
|
232
|
-
if (fromExclusive > lastIndex) {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
const isNewRowAt = (offset) => {
|
|
236
|
-
const top = offsetToTop(offset);
|
|
237
|
-
return top !== null && Math.abs(top - rowTop) > WRAP_THRESHOLD_PX;
|
|
238
|
-
};
|
|
239
|
-
// Exponential search to find a point that lands on the next row.
|
|
240
|
-
let step = 1;
|
|
241
|
-
let lastSame = fromExclusive - 1;
|
|
242
|
-
let probe = fromExclusive;
|
|
243
|
-
while (probe <= lastIndex) {
|
|
244
|
-
if (isNewRowAt(probe)) {
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
lastSame = probe;
|
|
248
|
-
probe += step;
|
|
249
|
-
step *= 2;
|
|
250
|
-
}
|
|
251
|
-
if (probe > lastIndex) {
|
|
252
|
-
probe = lastIndex;
|
|
253
|
-
if (!isNewRowAt(probe)) {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Binary search for the first offset whose top differs (lower_bound).
|
|
258
|
-
let low = Math.max(fromExclusive, lastSame + 1);
|
|
259
|
-
let high = probe;
|
|
260
|
-
while (low < high) {
|
|
261
|
-
const mid = (low + high) >>> 1;
|
|
262
|
-
if (isNewRowAt(mid)) {
|
|
263
|
-
high = mid;
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
low = mid + 1;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return low < params.lineLength ? low : null;
|
|
270
|
-
}
|
|
271
|
-
// Measure row boundaries using a log-time search per row rather than a full
|
|
272
|
-
// per-character scan (important for very long lines).
|
|
273
|
-
const rows = [];
|
|
274
|
-
const firstMeasurable = findFirstMeasurableOffset(0);
|
|
275
|
-
if (firstMeasurable === null) {
|
|
276
|
-
return [
|
|
277
|
-
{
|
|
278
|
-
startOffset: 0,
|
|
279
|
-
endOffset: params.lineLength,
|
|
280
|
-
rect: fallbackLineBox,
|
|
281
|
-
},
|
|
282
|
-
];
|
|
283
|
-
}
|
|
284
|
-
let currentRowStart = 0;
|
|
285
|
-
let currentRowTop = fullLineRects[0]?.top ?? params.lineRect.top;
|
|
286
|
-
let searchFrom = firstMeasurable + 1;
|
|
287
|
-
let rowIndex = 0;
|
|
288
|
-
while (currentRowStart < params.lineLength) {
|
|
289
|
-
const nextRowStart = findNextRowStartOffset(searchFrom, currentRowTop);
|
|
290
|
-
const currentRowEnd = nextRowStart ?? params.lineLength;
|
|
291
|
-
const domRect = rowRects[rowIndex] ?? params.lineRect;
|
|
292
|
-
rows.push({
|
|
293
|
-
startOffset: currentRowStart,
|
|
294
|
-
endOffset: currentRowEnd,
|
|
295
|
-
rect: toLayoutRect({
|
|
296
|
-
rect: domRect,
|
|
297
|
-
containerRect: params.containerRect,
|
|
298
|
-
scroll: params.scroll,
|
|
299
|
-
}),
|
|
300
|
-
});
|
|
301
|
-
if (nextRowStart === null) {
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
currentRowStart = nextRowStart;
|
|
305
|
-
rowIndex += 1;
|
|
306
|
-
const nextMeasurable = findFirstMeasurableOffset(currentRowStart);
|
|
307
|
-
if (nextMeasurable === null) {
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
currentRowTop = fullLineRects[rowIndex]?.top ?? currentRowTop;
|
|
311
|
-
searchFrom = nextMeasurable + 1;
|
|
312
|
-
}
|
|
313
|
-
// For single-row lines, use the fallback line box dimensions for consistency
|
|
314
|
-
if (rows.length === 1) {
|
|
315
|
-
rows[0] = {
|
|
316
|
-
...rows[0],
|
|
317
|
-
rect: {
|
|
318
|
-
...rows[0].rect,
|
|
319
|
-
top: fallbackLineBox.top,
|
|
320
|
-
height: fallbackLineBox.height,
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
return rows;
|
|
325
|
-
}
|
|
326
|
-
export function createDomLayoutMeasurer(params) {
|
|
327
|
-
const initialContainerRect = params.container.getBoundingClientRect();
|
|
328
|
-
if (!Number.isFinite(initialContainerRect.width)) {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
container: {
|
|
333
|
-
top: 0,
|
|
334
|
-
left: 0,
|
|
335
|
-
width: initialContainerRect.width,
|
|
336
|
-
height: initialContainerRect.height,
|
|
337
|
-
},
|
|
338
|
-
measureLine: ({ lineIndex, lineLength, top }) => {
|
|
339
|
-
const lineInfo = params.lines[lineIndex];
|
|
340
|
-
const containerRect = params.container.getBoundingClientRect();
|
|
341
|
-
const scroll = {
|
|
342
|
-
top: params.container.scrollTop,
|
|
343
|
-
left: params.container.scrollLeft,
|
|
344
|
-
};
|
|
345
|
-
const lineElement = getLineElement(params.root, lineIndex);
|
|
346
|
-
if (!lineElement || !lineInfo) {
|
|
347
|
-
return {
|
|
348
|
-
lineBox: {
|
|
349
|
-
top,
|
|
350
|
-
left: 0,
|
|
351
|
-
width: containerRect.width,
|
|
352
|
-
height: 0,
|
|
353
|
-
},
|
|
354
|
-
rows: [],
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
const lineRect = lineElement.getBoundingClientRect();
|
|
358
|
-
return {
|
|
359
|
-
lineBox: toLayoutRect({ rect: lineRect, containerRect, scroll }),
|
|
360
|
-
rows: measureLineRows({
|
|
361
|
-
lineElement,
|
|
362
|
-
lineLength,
|
|
363
|
-
lineRect,
|
|
364
|
-
containerRect,
|
|
365
|
-
scroll,
|
|
366
|
-
cursorToCodeUnit: lineInfo.cursorToCodeUnit,
|
|
367
|
-
codeUnitLength: lineInfo.text.length,
|
|
368
|
-
}),
|
|
369
|
-
};
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
export function measureLayoutModelFromDom(params) {
|
|
374
|
-
const measurer = createDomLayoutMeasurer({
|
|
375
|
-
root: params.root,
|
|
376
|
-
container: params.container,
|
|
377
|
-
lines: params.lines,
|
|
378
|
-
});
|
|
379
|
-
if (!measurer) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
return buildLayoutModel(params.lines, measurer);
|
|
383
|
-
}
|
|
384
|
-
export function measureLayoutModelRangeFromDom(params) {
|
|
385
|
-
const measurer = createDomLayoutMeasurer({
|
|
386
|
-
root: params.root,
|
|
387
|
-
container: params.container,
|
|
388
|
-
lines: params.lines,
|
|
389
|
-
});
|
|
390
|
-
if (!measurer) {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
const clampedStart = Math.max(0, Math.min(params.startLineIndex, params.lines.length - 1));
|
|
394
|
-
const clampedEnd = Math.max(clampedStart, Math.min(params.endLineIndex, params.lines.length - 1));
|
|
395
|
-
const lineOffsets = getLineOffsets(params.lines);
|
|
396
|
-
let lineStartOffset = lineOffsets[clampedStart] ?? 0;
|
|
397
|
-
const layouts = [];
|
|
398
|
-
for (let lineIndex = clampedStart; lineIndex <= clampedEnd; lineIndex += 1) {
|
|
399
|
-
const lineInfo = params.lines[lineIndex];
|
|
400
|
-
if (!lineInfo) {
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
const measurement = measurer.measureLine({
|
|
404
|
-
lineIndex: lineInfo.lineIndex,
|
|
405
|
-
lineText: lineInfo.text,
|
|
406
|
-
lineLength: lineInfo.cursorLength,
|
|
407
|
-
lineHasNewline: lineInfo.hasNewline,
|
|
408
|
-
top: 0,
|
|
409
|
-
});
|
|
410
|
-
layouts.push({
|
|
411
|
-
lineIndex: lineInfo.lineIndex,
|
|
412
|
-
lineStartOffset,
|
|
413
|
-
lineLength: lineInfo.cursorLength,
|
|
414
|
-
lineHasNewline: lineInfo.hasNewline,
|
|
415
|
-
lineBox: measurement.lineBox,
|
|
416
|
-
rows: measurement.rows,
|
|
417
|
-
});
|
|
418
|
-
lineStartOffset += lineInfo.cursorLength + (lineInfo.hasNewline ? 1 : 0);
|
|
419
|
-
}
|
|
420
|
-
return { container: measurer.container, lines: layouts };
|
|
421
|
-
}
|
|
422
|
-
export function getLineElement(root, lineIndex) {
|
|
423
|
-
return root.querySelector(`[data-line-index="${lineIndex}"]`);
|
|
424
|
-
}
|
|
425
|
-
function createTextWalker(root) {
|
|
426
|
-
return document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
427
|
-
}
|
|
428
|
-
export function resolveDomPosition(lineElement, offsetInLine) {
|
|
429
|
-
const walker = createTextWalker(lineElement);
|
|
430
|
-
let remaining = offsetInLine;
|
|
431
|
-
let current = walker.nextNode();
|
|
432
|
-
while (current) {
|
|
433
|
-
const length = current.textContent?.length ?? 0;
|
|
434
|
-
if (remaining <= length) {
|
|
435
|
-
return { node: current, offset: remaining };
|
|
436
|
-
}
|
|
437
|
-
remaining -= length;
|
|
438
|
-
current = walker.nextNode();
|
|
439
|
-
}
|
|
440
|
-
if (!lineElement.textContent) {
|
|
441
|
-
return { node: lineElement, offset: 0 };
|
|
442
|
-
}
|
|
443
|
-
return { node: lineElement, offset: lineElement.childNodes.length };
|
|
444
|
-
}
|
|
445
|
-
export function createOffsetToXMeasurer(params) {
|
|
446
|
-
const { root, container, lines } = params;
|
|
447
|
-
const containerRect = container.getBoundingClientRect();
|
|
448
|
-
const scrollLeft = container.scrollLeft;
|
|
449
|
-
return (lineIndex, offsetInLine) => {
|
|
450
|
-
const lineElement = getLineElement(root, lineIndex);
|
|
451
|
-
const lineInfo = lines[lineIndex];
|
|
452
|
-
if (!lineElement || !lineInfo) {
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
const codeUnitOffset = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, offsetInLine);
|
|
456
|
-
const position = resolveDomPosition(lineElement, codeUnitOffset);
|
|
457
|
-
const range = document.createRange();
|
|
458
|
-
range.setStart(position.node, position.offset);
|
|
459
|
-
range.setEnd(position.node, position.offset);
|
|
460
|
-
const rects = range.getClientRects();
|
|
461
|
-
if (rects.length > 0) {
|
|
462
|
-
const rect = rects[0];
|
|
463
|
-
return rect.left - containerRect.left + scrollLeft;
|
|
464
|
-
}
|
|
465
|
-
const boundingRect = range.getBoundingClientRect();
|
|
466
|
-
if (boundingRect.width === 0 && boundingRect.left !== 0) {
|
|
467
|
-
return boundingRect.left - containerRect.left + scrollLeft;
|
|
468
|
-
}
|
|
469
|
-
return null;
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
export function cursorOffsetToDomOffset(cursorToCodeUnit, offset) {
|
|
473
|
-
return cursorOffsetToCodeUnit(cursorToCodeUnit, offset);
|
|
474
|
-
}
|
|
475
|
-
export function hitTestFromLayout(params) {
|
|
476
|
-
const { clientX, clientY, root, container, lines } = params;
|
|
477
|
-
const layout = measureLayoutModelFromDom({ lines, root, container });
|
|
478
|
-
if (!layout || layout.lines.length === 0) {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
const containerRect = container.getBoundingClientRect();
|
|
482
|
-
const scroll = { top: container.scrollTop, left: container.scrollLeft };
|
|
483
|
-
// Convert client coordinates to container-relative coordinates
|
|
484
|
-
const relativeX = clientX - containerRect.left + scroll.left;
|
|
485
|
-
const relativeY = clientY - containerRect.top + scroll.top;
|
|
486
|
-
// Collect all rows from all lines with their document offsets
|
|
487
|
-
const allRows = [];
|
|
488
|
-
for (const lineLayout of layout.lines) {
|
|
489
|
-
for (const row of lineLayout.rows) {
|
|
490
|
-
const centerY = row.rect.top + row.rect.height / 2;
|
|
491
|
-
allRows.push({
|
|
492
|
-
lineIndex: lineLayout.lineIndex,
|
|
493
|
-
lineStartOffset: lineLayout.lineStartOffset,
|
|
494
|
-
row,
|
|
495
|
-
centerY,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (allRows.length === 0) {
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
// Find the closest row by Y center distance
|
|
503
|
-
let targetRowInfo = allRows[0];
|
|
504
|
-
let smallestCenterDistance = Math.abs(relativeY - targetRowInfo.centerY);
|
|
505
|
-
for (let i = 1; i < allRows.length; i++) {
|
|
506
|
-
const rowInfo = allRows[i];
|
|
507
|
-
const distance = Math.abs(relativeY - rowInfo.centerY);
|
|
508
|
-
if (distance < smallestCenterDistance) {
|
|
509
|
-
smallestCenterDistance = distance;
|
|
510
|
-
targetRowInfo = rowInfo;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const { lineIndex, lineStartOffset, row } = targetRowInfo;
|
|
514
|
-
const lineInfo = lines[lineIndex];
|
|
515
|
-
if (!lineInfo) {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
const lineElement = getLineElement(root, lineIndex);
|
|
519
|
-
if (!lineElement) {
|
|
520
|
-
return null;
|
|
521
|
-
}
|
|
522
|
-
// Find the closest cursor offset by X within the target row.
|
|
523
|
-
//
|
|
524
|
-
// Measuring per-character client rects is brittle across engines at text-node
|
|
525
|
-
// boundaries (e.g. when a trailing space lives in a text node that ends right
|
|
526
|
-
// before an inline wrapper like <a>). Prefer collapsed ranges (caret positions)
|
|
527
|
-
// and pick the fragment that belongs to the target visual row.
|
|
528
|
-
const resolvePosition = createDomPositionResolver(lineElement);
|
|
529
|
-
const scratchRange = document.createRange();
|
|
530
|
-
const rowTop = row.rect.top;
|
|
531
|
-
const approximateX = (cursorOffsetInLine) => {
|
|
532
|
-
const clamped = Math.max(row.startOffset, Math.min(cursorOffsetInLine, row.endOffset));
|
|
533
|
-
const rowLength = row.endOffset - row.startOffset;
|
|
534
|
-
if (rowLength <= 0) {
|
|
535
|
-
return row.rect.left;
|
|
536
|
-
}
|
|
537
|
-
const fraction = (clamped - row.startOffset) / rowLength;
|
|
538
|
-
return row.rect.left + row.rect.width * fraction;
|
|
539
|
-
};
|
|
540
|
-
const measureCaretXOnRow = (cursorOffsetInLine) => {
|
|
541
|
-
const maxRowTopDelta = Math.max(2, row.rect.height / 2);
|
|
542
|
-
const measureCharEdgeX = (from, to, edge) => {
|
|
543
|
-
const fromCodeUnit = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, from);
|
|
544
|
-
const toCodeUnit = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, to);
|
|
545
|
-
const fromPos = resolvePosition(fromCodeUnit);
|
|
546
|
-
const toPos = resolvePosition(toCodeUnit);
|
|
547
|
-
// When the measured character boundary spans across text nodes (e.g. a
|
|
548
|
-
// trailing space in one node right before an inline wrapper), some engines
|
|
549
|
-
// can return empty rect lists for a cross-node range. Prefer measuring
|
|
550
|
-
// inside the end node when the boundary lands at the end of the start node.
|
|
551
|
-
if (fromPos.node !== toPos.node &&
|
|
552
|
-
fromPos.node instanceof Text &&
|
|
553
|
-
toPos.node instanceof Text &&
|
|
554
|
-
fromPos.offset === fromPos.node.length &&
|
|
555
|
-
toPos.offset > 0) {
|
|
556
|
-
scratchRange.setStart(toPos.node, 0);
|
|
557
|
-
scratchRange.setEnd(toPos.node, toPos.offset);
|
|
558
|
-
}
|
|
559
|
-
else {
|
|
560
|
-
scratchRange.setStart(fromPos.node, fromPos.offset);
|
|
561
|
-
scratchRange.setEnd(toPos.node, toPos.offset);
|
|
562
|
-
}
|
|
563
|
-
const rects = scratchRange.getClientRects();
|
|
564
|
-
const list = rects.length > 0
|
|
565
|
-
? Array.from(rects)
|
|
566
|
-
: (() => {
|
|
567
|
-
const rect = scratchRange.getBoundingClientRect();
|
|
568
|
-
return rect.width === 0 && rect.height === 0 ? [] : [rect];
|
|
569
|
-
})();
|
|
570
|
-
if (list.length === 0) {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
// Pick rects from the visual row closest to `rowTop`, then take the
|
|
574
|
-
// extreme edge across those rects. This avoids zero-width fragments
|
|
575
|
-
// skewing the result (WebKit can include those at wrap boundaries).
|
|
576
|
-
const DIST_EPS = 0.01;
|
|
577
|
-
let bestTopDistance = Number.POSITIVE_INFINITY;
|
|
578
|
-
const candidates = [];
|
|
579
|
-
for (const rect of list) {
|
|
580
|
-
const top = rect.top - containerRect.top + scroll.top;
|
|
581
|
-
const distance = Math.abs(top - rowTop);
|
|
582
|
-
if (distance + DIST_EPS < bestTopDistance) {
|
|
583
|
-
bestTopDistance = distance;
|
|
584
|
-
candidates.length = 0;
|
|
585
|
-
candidates.push(rect);
|
|
586
|
-
}
|
|
587
|
-
else if (Math.abs(distance - bestTopDistance) <= DIST_EPS) {
|
|
588
|
-
candidates.push(rect);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (candidates.length === 0 || bestTopDistance > maxRowTopDelta) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
if (edge === "left") {
|
|
595
|
-
let left = candidates[0].left;
|
|
596
|
-
for (const rect of candidates) {
|
|
597
|
-
left = Math.min(left, rect.left);
|
|
598
|
-
}
|
|
599
|
-
return left - containerRect.left + scroll.left;
|
|
600
|
-
}
|
|
601
|
-
let right = candidates[0].right;
|
|
602
|
-
for (const rect of candidates) {
|
|
603
|
-
right = Math.max(right, rect.right);
|
|
604
|
-
}
|
|
605
|
-
return right - containerRect.left + scroll.left;
|
|
606
|
-
};
|
|
607
|
-
// Prefer deriving boundary X from a neighboring character's rect. This is
|
|
608
|
-
// more stable than collapsed-caret rects at wrap boundaries across engines.
|
|
609
|
-
//
|
|
610
|
-
// WebKit can report "trailing" spaces at the end of a text node (right before
|
|
611
|
-
// an inline element like <a>) with a zero-width rect whose right edge equals
|
|
612
|
-
// the previous character's boundary. In that case the caret boundary after
|
|
613
|
-
// the space must be derived from the next visible character instead.
|
|
614
|
-
const prevChar = cursorOffsetInLine > 0
|
|
615
|
-
? (lineInfo.text[cursorOffsetInLine - 1] ?? "")
|
|
616
|
-
: "";
|
|
617
|
-
const preferNextForPrevWhitespace = cursorOffsetInLine > row.startOffset &&
|
|
618
|
-
cursorOffsetInLine < row.endOffset &&
|
|
619
|
-
/\s/.test(prevChar);
|
|
620
|
-
if (preferNextForPrevWhitespace) {
|
|
621
|
-
const xNext = measureCharEdgeX(cursorOffsetInLine, cursorOffsetInLine + 1, "left");
|
|
622
|
-
if (xNext !== null) {
|
|
623
|
-
return xNext;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if (cursorOffsetInLine > row.startOffset) {
|
|
627
|
-
const xPrev = measureCharEdgeX(cursorOffsetInLine - 1, cursorOffsetInLine, "right");
|
|
628
|
-
if (xPrev !== null) {
|
|
629
|
-
return xPrev;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
if (cursorOffsetInLine < row.endOffset) {
|
|
633
|
-
const xNext = measureCharEdgeX(cursorOffsetInLine, cursorOffsetInLine + 1, "left");
|
|
634
|
-
if (xNext !== null) {
|
|
635
|
-
return xNext;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const codeUnitOffset = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, cursorOffsetInLine);
|
|
639
|
-
const position = resolvePosition(codeUnitOffset);
|
|
640
|
-
scratchRange.setStart(position.node, position.offset);
|
|
641
|
-
scratchRange.setEnd(position.node, position.offset);
|
|
642
|
-
const rects = scratchRange.getClientRects();
|
|
643
|
-
const candidates = rects.length > 0
|
|
644
|
-
? Array.from(rects)
|
|
645
|
-
: [scratchRange.getBoundingClientRect()];
|
|
646
|
-
if (candidates.length === 0) {
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
let best = candidates[0];
|
|
650
|
-
let bestDistance = Number.POSITIVE_INFINITY;
|
|
651
|
-
for (const rect of candidates) {
|
|
652
|
-
const top = rect.top - containerRect.top + scroll.top;
|
|
653
|
-
const distance = Math.abs(top - rowTop);
|
|
654
|
-
if (distance < bestDistance) {
|
|
655
|
-
bestDistance = distance;
|
|
656
|
-
best = rect;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// If the caret rect we got belongs to a different visual row (common at wrap
|
|
660
|
-
// boundaries in some engines), fall back to the layout-based approximation
|
|
661
|
-
// to keep X monotonic within this row.
|
|
662
|
-
if (bestDistance > maxRowTopDelta) {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
if (best.height <= 0) {
|
|
666
|
-
return null;
|
|
667
|
-
}
|
|
668
|
-
return best.left - containerRect.left + scroll.left;
|
|
669
|
-
};
|
|
670
|
-
const caretX = (cursorOffsetInLine) => {
|
|
671
|
-
return measureCaretXOnRow(cursorOffsetInLine) ?? approximateX(cursorOffsetInLine);
|
|
672
|
-
};
|
|
673
|
-
// Check if the click is outside the row's horizontal bounds (i.e. in container padding).
|
|
674
|
-
// If so, snap directly to the row edge rather than searching for a character.
|
|
675
|
-
const rowLeftEdge = row.rect.left;
|
|
676
|
-
const rowRightEdge = row.rect.left + row.rect.width;
|
|
677
|
-
if (relativeX < rowLeftEdge) {
|
|
678
|
-
// Click is in the left padding area - snap to start of row
|
|
679
|
-
return {
|
|
680
|
-
cursorOffset: lineStartOffset + row.startOffset,
|
|
681
|
-
pastRowEnd: false,
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
if (relativeX > rowRightEdge) {
|
|
685
|
-
// Click is in the right padding area - snap to end of row
|
|
686
|
-
// Use backward affinity (pastRowEnd) when not at the logical line end
|
|
687
|
-
const isEndOfLine = row.endOffset === lineInfo.cursorLength;
|
|
688
|
-
return {
|
|
689
|
-
cursorOffset: lineStartOffset + row.endOffset,
|
|
690
|
-
pastRowEnd: !isEndOfLine && row.endOffset < lineInfo.cursorLength,
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
// Binary search the insertion point for relativeX among monotonic caret Xs.
|
|
694
|
-
let low = row.startOffset;
|
|
695
|
-
let high = row.endOffset;
|
|
696
|
-
while (low < high) {
|
|
697
|
-
const mid = (low + high) >>> 1;
|
|
698
|
-
const xMid = caretX(mid);
|
|
699
|
-
if (xMid < relativeX) {
|
|
700
|
-
low = mid + 1;
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
high = mid;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const candidateA = Math.max(row.startOffset, Math.min(low, row.endOffset));
|
|
707
|
-
const candidateB = Math.max(row.startOffset, Math.min(candidateA - 1, row.endOffset));
|
|
708
|
-
const xA = caretX(candidateA);
|
|
709
|
-
const xB = caretX(candidateB);
|
|
710
|
-
const distA = Math.abs(relativeX - xA);
|
|
711
|
-
const distB = Math.abs(relativeX - xB);
|
|
712
|
-
const DIST_EPS_PX = 0.5;
|
|
713
|
-
let closestOffset = (() => {
|
|
714
|
-
if (distB + DIST_EPS_PX < distA) {
|
|
715
|
-
return candidateB;
|
|
716
|
-
}
|
|
717
|
-
if (distA + DIST_EPS_PX < distB) {
|
|
718
|
-
return candidateA;
|
|
719
|
-
}
|
|
720
|
-
// Near-ties: decide by midpoint between the two caret boundaries. This is
|
|
721
|
-
// stable for narrow glyphs where subpixel jitter and integer mouse coords
|
|
722
|
-
// would otherwise make left/right clicks ambiguous.
|
|
723
|
-
const mid = (xA + xB) / 2;
|
|
724
|
-
// Prefer the *earlier* boundary at an exact midpoint tie. This matches
|
|
725
|
-
// typical editor caret behavior and avoids center-click instability across
|
|
726
|
-
// engines for monospaced glyphs.
|
|
727
|
-
return relativeX > mid ? candidateA : candidateB;
|
|
728
|
-
})();
|
|
729
|
-
const endX = caretX(row.endOffset);
|
|
730
|
-
// Normalize within runs of *source-only* offsets that collapse to the same DOM
|
|
731
|
-
// caret position (e.g. "[" in a markdown link). Multiple cursor offsets may map
|
|
732
|
-
// to the same code unit offset; in that case clicks should prefer the earliest
|
|
733
|
-
// cursor offset in that collapsed run.
|
|
734
|
-
//
|
|
735
|
-
// IMPORTANT: do not collapse offsets purely by X distance — engines may report
|
|
736
|
-
// identical X for distinct, measurable caret boundaries (notably around trailing
|
|
737
|
-
// spaces at inline boundaries in WebKit). Use code-unit equality as the signal
|
|
738
|
-
// that the offsets are truly indistinguishable in the DOM.
|
|
739
|
-
{
|
|
740
|
-
const baseMeasuredX = measureCaretXOnRow(closestOffset);
|
|
741
|
-
if (baseMeasuredX !== null) {
|
|
742
|
-
const baseCodeUnit = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, closestOffset);
|
|
743
|
-
const COLLAPSE_EPS_PX = 0.25;
|
|
744
|
-
while (closestOffset > row.startOffset) {
|
|
745
|
-
const prev = closestOffset - 1;
|
|
746
|
-
const prevCodeUnit = cursorOffsetToCodeUnit(lineInfo.cursorToCodeUnit, prev);
|
|
747
|
-
if (prevCodeUnit !== baseCodeUnit) {
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
const prevMeasuredX = measureCaretXOnRow(prev);
|
|
751
|
-
if (prevMeasuredX === null) {
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
if (Math.abs(prevMeasuredX - baseMeasuredX) > COLLAPSE_EPS_PX) {
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
closestOffset = prev;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const boundaryX = caretX(closestOffset);
|
|
762
|
-
// If the click lands on the "left" side of the chosen caret boundary, treat it
|
|
763
|
-
// as selecting the right edge of the previous character (backward affinity).
|
|
764
|
-
// This preserves expected wrapper behavior at boundaries (e.g. bold continues
|
|
765
|
-
// when clicking the right side of a bold character).
|
|
766
|
-
const choseRightEdge = closestOffset > row.startOffset && relativeX < boundaryX - 0.01;
|
|
767
|
-
// Determine if caret should have backward affinity when clicking past the end
|
|
768
|
-
// of a wrapped visual row (i.e. beyond the row-end caret). EXCEPT: at end of
|
|
769
|
-
// the logical line, prefer forward affinity (v1 parity: exit formatting).
|
|
770
|
-
const isEndOfLine = row.endOffset === lineInfo.cursorLength;
|
|
771
|
-
const atLineEnd = closestOffset === row.endOffset && isEndOfLine;
|
|
772
|
-
const pastRowEnd = (!atLineEnd && choseRightEdge) ||
|
|
773
|
-
(!atLineEnd &&
|
|
774
|
-
closestOffset === row.endOffset &&
|
|
775
|
-
row.endOffset < lineInfo.cursorLength &&
|
|
776
|
-
relativeX > endX + 0.5);
|
|
777
|
-
return {
|
|
778
|
-
cursorOffset: lineStartOffset + closestOffset,
|
|
779
|
-
pastRowEnd,
|
|
780
|
-
};
|
|
781
|
-
}
|