@adecore/editor 0.22.1 → 0.23.1
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/attribution.d.ts +9 -8
- package/dist/attribution.js +44 -9
- package/dist/bidi.d.ts +44 -0
- package/dist/bidi.js +26 -0
- package/dist/column-selection.js +2 -1
- package/dist/controller.d.ts +6 -1
- package/dist/controller.js +93 -3
- package/dist/editor.css +81 -1
- package/dist/engine.js +189 -41
- package/dist/fake.d.ts +45 -5
- package/dist/fake.js +121 -13
- package/dist/find-search.d.ts +26 -0
- package/dist/find-search.js +130 -0
- package/dist/find.d.ts +50 -2
- package/dist/find.js +575 -25
- package/dist/geometry-stops.d.ts +6 -0
- package/dist/geometry-stops.js +47 -0
- package/dist/grammar-preparation.d.ts +29 -0
- package/dist/grammar-preparation.js +147 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +10 -1
- package/dist/keymap-table.js +37 -1
- package/dist/keymap.d.ts +1 -0
- package/dist/keymap.js +4 -1
- package/dist/layout-changes.d.ts +6 -0
- package/dist/layout-changes.js +27 -0
- package/dist/layout.d.ts +45 -2
- package/dist/layout.js +202 -58
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/outline.js +5 -4
- package/dist/paint.d.ts +11 -3
- package/dist/paint.js +180 -34
- package/dist/search-client.d.ts +27 -0
- package/dist/search-client.js +210 -0
- package/dist/search-protocol.d.ts +103 -0
- package/dist/search-protocol.js +3 -0
- package/dist/search-worker.d.ts +11 -0
- package/dist/search-worker.js +137 -0
- package/dist/semantic.d.ts +2 -1
- package/dist/semantic.js +6 -1
- package/dist/shiki-document.d.ts +8 -0
- package/dist/shiki-document.js +93 -0
- package/dist/shiki-worker.d.ts +65 -0
- package/dist/shiki-worker.js +265 -0
- package/dist/shiki.d.ts +1 -0
- package/dist/shiki.js +96 -33
- package/dist/testing.d.ts +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +37 -7
- package/dist/touch.d.ts +27 -0
- package/dist/touch.js +100 -0
- package/dist/types.d.ts +86 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +69 -17
- package/dist/view.js +427 -97
- package/dist/worker-client.d.ts +37 -0
- package/dist/worker-client.js +195 -0
- package/package.json +17 -2
- package/src/attribution.ts +50 -14
- package/src/bidi.ts +32 -0
- package/src/column-selection.ts +2 -1
- package/src/controller.ts +97 -3
- package/src/editor.css +81 -1
- package/src/engine.ts +222 -42
- package/src/fake.ts +157 -14
- package/src/find-search.ts +165 -0
- package/src/find.ts +622 -21
- package/src/geometry-stops.ts +51 -0
- package/src/grammar-preparation.ts +158 -0
- package/src/index.ts +10 -0
- package/src/keymap-table.ts +44 -2
- package/src/keymap.ts +5 -1
- package/src/layout-changes.ts +33 -0
- package/src/layout.ts +262 -63
- package/src/offsets.ts +24 -0
- package/src/outline.ts +5 -4
- package/src/paint.ts +202 -36
- package/src/search-client.ts +306 -0
- package/src/search-protocol.ts +100 -0
- package/src/search-worker.ts +171 -0
- package/src/semantic.ts +8 -2
- package/src/shiki-document.ts +143 -0
- package/src/shiki-worker.ts +344 -0
- package/src/shiki.ts +103 -33
- package/src/testing.ts +2 -0
- package/src/tokens.ts +36 -7
- package/src/touch.ts +134 -0
- package/src/types.ts +178 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +505 -102
- package/src/worker-client.ts +212 -0
package/src/layout.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
import type { Position } from '@adecore/editor-core';
|
|
1
|
+
import type { DocumentChange, Position } from '@adecore/editor-core';
|
|
2
|
+
|
|
3
|
+
import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geometry-stops.ts';
|
|
4
|
+
import { BIDI_COLUMNS, bidiControl, visibleBidi } from './bidi.ts';
|
|
5
|
+
import { uniformGeometry } from './uniform-geometry.ts';
|
|
6
|
+
import { lineChangeMapper } from './layout-changes.ts';
|
|
2
7
|
|
|
3
8
|
export type Affinity = 'before' | 'after';
|
|
4
9
|
|
|
10
|
+
export interface VerticalWindow {
|
|
11
|
+
top: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubRowBand {
|
|
16
|
+
first: number;
|
|
17
|
+
last: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function subRowBand(top: number, subRows: number, lineHeight: number, window: VerticalWindow, block = 1): SubRowBand {
|
|
21
|
+
const first = Math.max(0, Math.min(subRows - 1, Math.floor((window.top - top) / lineHeight)));
|
|
22
|
+
const last = Math.max(first, Math.min(subRows - 1, Math.ceil((window.bottom - top) / lineHeight) - 1));
|
|
23
|
+
return {
|
|
24
|
+
first: Math.floor(first / block) * block,
|
|
25
|
+
last: Math.min(subRows - 1, Math.ceil((last + 1) / block) * block - 1)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
export interface LayoutDocument {
|
|
6
30
|
getLineCount(): number;
|
|
7
31
|
getLine(line: number): { start: number; end: number; next: number; text: string };
|
|
@@ -22,6 +46,11 @@ export interface Inlay {
|
|
|
22
46
|
at: number;
|
|
23
47
|
text: string;
|
|
24
48
|
tooltip?: string;
|
|
49
|
+
/* The text cut into pieces drawn as elements of their own; they add up to `text`. */
|
|
50
|
+
parts?: readonly string[];
|
|
51
|
+
/* Pixels of space kept clear before and after the pill. */
|
|
52
|
+
gapLeft?: number;
|
|
53
|
+
gapRight?: number;
|
|
25
54
|
/* What an input method has composed: drawn as text in the code face, with no padding, so what stands around it does not move. */
|
|
26
55
|
composition?: boolean;
|
|
27
56
|
/* A suggestion the host drew after the caret: text in the code face with no padding, which the caret stays in front of. */
|
|
@@ -40,6 +69,26 @@ export interface BlockWidget {
|
|
|
40
69
|
revision?: number;
|
|
41
70
|
/* A quiet row of one code line's height, placed at the indentation of its line and never measured. */
|
|
42
71
|
lens?: boolean;
|
|
72
|
+
fixed?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The lines from `from` through `to` drawn as a row of the host's own DOM, as tall as the host makes it, while the text stays in the document. */
|
|
76
|
+
export interface BlockReplacement {
|
|
77
|
+
id: string;
|
|
78
|
+
/* Offsets inside the first and the last line it stands for. */
|
|
79
|
+
from: number;
|
|
80
|
+
to: number;
|
|
81
|
+
render: (container: HTMLElement) => void;
|
|
82
|
+
/* The height until the row has been measured. */
|
|
83
|
+
height?: number;
|
|
84
|
+
/* Drawn again in the element it already has when this changes, which `render` alone never is. */
|
|
85
|
+
revision?: number;
|
|
86
|
+
/* What assistive technology calls the row. */
|
|
87
|
+
label?: string;
|
|
88
|
+
/* Whether vertical caret movement may stop in the row's lines instead of stepping over the row. */
|
|
89
|
+
enterable?: boolean;
|
|
90
|
+
/* Called before the view scrolls to an offset inside the row's lines. */
|
|
91
|
+
onReveal?: () => void;
|
|
43
92
|
}
|
|
44
93
|
|
|
45
94
|
/* Collapsed, it hides the lines after `startLine` through `endLine`. */
|
|
@@ -72,6 +121,8 @@ export interface TextRow extends RowBase {
|
|
|
72
121
|
head: number | null;
|
|
73
122
|
/* Visual lines, which is more than one for a wrapped line. */
|
|
74
123
|
subRows: number;
|
|
124
|
+
/* Set when the host draws the lines `line` through `lastLine` itself; the row is then as tall as its content. */
|
|
125
|
+
replacement?: BlockReplacement;
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
export interface BlockRow extends RowBase {
|
|
@@ -82,6 +133,7 @@ export interface BlockRow extends RowBase {
|
|
|
82
133
|
export type LayoutRow = TextRow | BlockRow;
|
|
83
134
|
|
|
84
135
|
export interface TextRun {
|
|
136
|
+
/* Line-relative offsets let preceding edits move a line without rewriting its runs. */
|
|
85
137
|
from: number;
|
|
86
138
|
to: number;
|
|
87
139
|
x: number;
|
|
@@ -101,8 +153,9 @@ export interface InlineBox {
|
|
|
101
153
|
export interface LineGeometry {
|
|
102
154
|
start: number;
|
|
103
155
|
end: number;
|
|
156
|
+
next: number;
|
|
104
157
|
text: string;
|
|
105
|
-
/*
|
|
158
|
+
/* Uniform ASCII rows share x prefixes instead of allocating these four arrays per character. */
|
|
106
159
|
offsets: number[];
|
|
107
160
|
/* The x of each stop before and after the inlays that sit on it. */
|
|
108
161
|
before: number[];
|
|
@@ -113,6 +166,9 @@ export interface LineGeometry {
|
|
|
113
166
|
rowStarts: number[];
|
|
114
167
|
/* The x each visual line ends at. */
|
|
115
168
|
rowEnds: number[];
|
|
169
|
+
/* The furthest stop a greedy wrap decision read, including its lookahead. */
|
|
170
|
+
rowTriggers: number[];
|
|
171
|
+
uniform?: { first: readonly number[]; continuation: readonly number[] };
|
|
116
172
|
runs: TextRun[];
|
|
117
173
|
inlays: InlineBox[];
|
|
118
174
|
subRows: number;
|
|
@@ -139,20 +195,6 @@ const PLAIN = /^[\x20-\x7e\t]*$/;
|
|
|
139
195
|
const GRAPHEME_LIMIT = 20000;
|
|
140
196
|
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
141
197
|
|
|
142
|
-
function lowerBound(values: readonly number[], value: number): number {
|
|
143
|
-
let low = 0;
|
|
144
|
-
let high = values.length;
|
|
145
|
-
while (low < high) {
|
|
146
|
-
const middle = (low + high) >>> 1;
|
|
147
|
-
if (values[middle]! < value) {
|
|
148
|
-
low = middle + 1;
|
|
149
|
-
} else {
|
|
150
|
-
high = middle;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return low;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
198
|
/* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
|
|
157
199
|
function characterOffsets(text: string): number[] {
|
|
158
200
|
const offsets: number[] = [];
|
|
@@ -178,7 +220,10 @@ function characterWidth(metrics: LayoutMetrics, text: string, x: number): number
|
|
|
178
220
|
const tab = metrics.tabSize * metrics.charWidth;
|
|
179
221
|
return (Math.floor(x / tab) + 1) * tab - x;
|
|
180
222
|
}
|
|
181
|
-
|
|
223
|
+
if (text.length === 1 && text >= ' ' && text <= '~') {
|
|
224
|
+
return metrics.charWidth;
|
|
225
|
+
}
|
|
226
|
+
return bidiControl(text) !== null ? BIDI_COLUMNS * metrics.charWidth : metrics.measureText(text);
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
/* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
|
|
@@ -197,20 +242,22 @@ export function wrapIndent(text: string, metrics: LayoutMetrics, limit: number):
|
|
|
197
242
|
* The stops at which a wrapped line starts a new visual line, after the space a word ends in when
|
|
198
243
|
* there is one on the visual line and in the middle of a word when there is not.
|
|
199
244
|
*/
|
|
200
|
-
|
|
245
|
+
function wrapRows(text: string, offsets: readonly number[] | null, metrics: LayoutMetrics, limit: number): { breaks: number[]; triggers: number[] } {
|
|
201
246
|
const indent = wrapIndent(text, metrics, limit);
|
|
202
247
|
const breaks: number[] = [];
|
|
203
|
-
const
|
|
248
|
+
const triggers: number[] = [];
|
|
249
|
+
const count = offsets === null ? text.length : offsets.length - 1;
|
|
204
250
|
let rowStart = 0;
|
|
205
251
|
let opportunity = -1;
|
|
206
252
|
let x = 0;
|
|
207
253
|
let index = 0;
|
|
208
254
|
while (index < count) {
|
|
209
|
-
const character = text.slice(offsets[index]!, offsets[index + 1]!);
|
|
255
|
+
const character = offsets === null ? text[index]! : text.slice(offsets[index]!, offsets[index + 1]!);
|
|
210
256
|
const width = characterWidth(metrics, character, x);
|
|
211
257
|
if (index > rowStart && x + width > limit && !isSpace(character)) {
|
|
212
258
|
const target = opportunity > rowStart ? opportunity : index;
|
|
213
259
|
breaks.push(target);
|
|
260
|
+
triggers.push(index);
|
|
214
261
|
rowStart = target;
|
|
215
262
|
opportunity = -1;
|
|
216
263
|
x = indent;
|
|
@@ -218,28 +265,35 @@ export function wrapStops(text: string, offsets: readonly number[], metrics: Lay
|
|
|
218
265
|
continue;
|
|
219
266
|
}
|
|
220
267
|
x += width;
|
|
221
|
-
if (isSpace(character) && index + 1 < count && !isSpace(text[offsets[index + 1]!])) {
|
|
268
|
+
if (isSpace(character) && index + 1 < count && !isSpace(text[offsets === null ? index + 1 : offsets[index + 1]!])) {
|
|
222
269
|
opportunity = index + 1;
|
|
223
270
|
}
|
|
224
271
|
index++;
|
|
225
272
|
}
|
|
226
|
-
|
|
273
|
+
triggers.push(count);
|
|
274
|
+
return { breaks, triggers };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function wrapStops(text: string, offsets: readonly number[], metrics: LayoutMetrics, limit: number): number[] {
|
|
278
|
+
return wrapRows(text, offsets, metrics, limit).breaks;
|
|
227
279
|
}
|
|
228
280
|
|
|
229
281
|
/* A line of text laid out against its metrics; wrapped at `limit` pixels unless that is null. */
|
|
230
|
-
export function
|
|
231
|
-
line: { start: number; end: number; text: string },
|
|
282
|
+
export function scanDenseLine(
|
|
283
|
+
line: { start: number; end: number; text: string; next?: number },
|
|
232
284
|
metrics: LayoutMetrics,
|
|
233
285
|
limit: number | null,
|
|
234
286
|
lineInlays: readonly Inlay[]
|
|
235
287
|
): LineGeometry {
|
|
236
288
|
const text = line.text;
|
|
237
289
|
const offsets = characterOffsets(text);
|
|
238
|
-
const
|
|
290
|
+
const wrapped = limit === null ? { breaks: [], triggers: [offsets.length - 1] } : wrapRows(text, offsets, metrics, limit);
|
|
291
|
+
const breakStops = wrapped.breaks;
|
|
239
292
|
const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
|
|
240
293
|
const geometry: LineGeometry = {
|
|
241
294
|
start: line.start,
|
|
242
295
|
end: line.end,
|
|
296
|
+
next: line.next ?? line.end,
|
|
243
297
|
text,
|
|
244
298
|
offsets,
|
|
245
299
|
before: [],
|
|
@@ -247,6 +301,7 @@ export function scanLine(
|
|
|
247
301
|
rowOf: [],
|
|
248
302
|
rowStarts: [0],
|
|
249
303
|
rowEnds: [],
|
|
304
|
+
rowTriggers: wrapped.triggers,
|
|
250
305
|
runs: [],
|
|
251
306
|
inlays: [],
|
|
252
307
|
subRows: breakStops.length + 1,
|
|
@@ -260,7 +315,7 @@ export function scanLine(
|
|
|
260
315
|
let runX = 0;
|
|
261
316
|
const flushRun = (end: number): void => {
|
|
262
317
|
if (runStart >= 0 && end > runStart) {
|
|
263
|
-
geometry.runs.push({ from:
|
|
318
|
+
geometry.runs.push({ from: runStart, to: end, x: runX, subRow: row, text: text.slice(runStart, end) });
|
|
264
319
|
}
|
|
265
320
|
runStart = -1;
|
|
266
321
|
};
|
|
@@ -279,7 +334,11 @@ export function scanLine(
|
|
|
279
334
|
while (nextInlay < lineInlays.length && lineInlays[nextInlay]!.at - line.start <= offset) {
|
|
280
335
|
flushRun(offset);
|
|
281
336
|
const inlay = lineInlays[nextInlay++]!;
|
|
282
|
-
const
|
|
337
|
+
const shown = visibleBidi(inlay.text);
|
|
338
|
+
const width =
|
|
339
|
+
inlay.composition || inlay.ghost
|
|
340
|
+
? metrics.measureText(shown)
|
|
341
|
+
: Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
|
|
283
342
|
geometry.inlays.push({ inlay, x, width, subRow: row });
|
|
284
343
|
x += width;
|
|
285
344
|
}
|
|
@@ -289,7 +348,7 @@ export function scanLine(
|
|
|
289
348
|
}
|
|
290
349
|
const character = text.slice(offset, offsets[stop + 1]!);
|
|
291
350
|
const width = characterWidth(metrics, character, x);
|
|
292
|
-
if (character === '\t') {
|
|
351
|
+
if (character === '\t' || bidiControl(character) !== null) {
|
|
293
352
|
flushRun(offset);
|
|
294
353
|
} else if (runStart < 0) {
|
|
295
354
|
runStart = offset;
|
|
@@ -299,19 +358,39 @@ export function scanLine(
|
|
|
299
358
|
}
|
|
300
359
|
flushRun(text.length);
|
|
301
360
|
geometry.rowEnds.push(x);
|
|
302
|
-
|
|
361
|
+
for (const end of geometry.rowEnds) {
|
|
362
|
+
geometry.width = Math.max(geometry.width, end);
|
|
363
|
+
}
|
|
303
364
|
return geometry;
|
|
304
365
|
}
|
|
305
366
|
|
|
367
|
+
export function scanLine(
|
|
368
|
+
line: { start: number; end: number; text: string; next?: number },
|
|
369
|
+
metrics: LayoutMetrics,
|
|
370
|
+
limit: number | null,
|
|
371
|
+
lineInlays: readonly Inlay[]
|
|
372
|
+
): LineGeometry {
|
|
373
|
+
if (limit !== null && line.text.length > GRAPHEME_LIMIT && lineInlays.length === 0 && /^[\x20-\x7e]*$/.test(line.text)) {
|
|
374
|
+
const wrapped = wrapRows(line.text, null, metrics, limit);
|
|
375
|
+
const geometry = uniformGeometry(line, metrics, wrapped.breaks, wrapped.triggers, wrapIndent(line.text, metrics, limit));
|
|
376
|
+
if (geometry !== null) {
|
|
377
|
+
return geometry;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return scanDenseLine(line, metrics, limit, lineInlays);
|
|
381
|
+
}
|
|
382
|
+
|
|
306
383
|
export interface LayoutConfig {
|
|
307
|
-
/*
|
|
384
|
+
/* Clear remembered geometry when transaction offsets are unavailable. */
|
|
308
385
|
textChanged?: boolean;
|
|
386
|
+
changes?: readonly (readonly DocumentChange[])[];
|
|
309
387
|
metrics?: LayoutMetrics;
|
|
310
388
|
/* The pixels a visual line may take before it wraps; null for no wrapping. */
|
|
311
389
|
wrapWidth?: number | null;
|
|
312
390
|
inlays?: readonly Inlay[];
|
|
313
391
|
blocks?: readonly BlockWidget[];
|
|
314
392
|
folds?: readonly FoldState[];
|
|
393
|
+
replacements?: readonly BlockReplacement[];
|
|
315
394
|
}
|
|
316
395
|
|
|
317
396
|
/*
|
|
@@ -322,16 +401,18 @@ export interface LayoutConfig {
|
|
|
322
401
|
export class EditorLayout {
|
|
323
402
|
rows: LayoutRow[] = [];
|
|
324
403
|
height = 0;
|
|
404
|
+
lineHeightsVersion = 0;
|
|
325
405
|
/* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
|
|
326
406
|
width = 0;
|
|
327
407
|
private lineRows: number[] = [];
|
|
328
408
|
private geometryCache = new Map<number, LineGeometry>();
|
|
329
|
-
private measuredRows = new Map<number, number>();
|
|
409
|
+
private measuredRows = new Map<number, { start: number; end: number; next: number; subRows: number }>();
|
|
330
410
|
private measuredHeights = new Map<string, number>();
|
|
331
411
|
private inlaysByLine = new Map<number, Inlay[]>();
|
|
332
412
|
private inlays: readonly Inlay[] = [];
|
|
333
413
|
private blocks: readonly BlockWidget[] = [];
|
|
334
414
|
private folds: readonly FoldState[] = [];
|
|
415
|
+
private replacements: readonly BlockReplacement[] = [];
|
|
335
416
|
private wrapWidth: number | null = null;
|
|
336
417
|
|
|
337
418
|
readonly document: LayoutDocument;
|
|
@@ -360,22 +441,80 @@ export class EditorLayout {
|
|
|
360
441
|
}
|
|
361
442
|
if (config.inlays) {
|
|
362
443
|
this.inlays = config.inlays;
|
|
363
|
-
measureAgain = true;
|
|
364
444
|
}
|
|
365
445
|
if (config.blocks) {
|
|
366
446
|
this.blocks = config.blocks;
|
|
367
447
|
}
|
|
368
448
|
if (config.folds) {
|
|
369
449
|
this.folds = config.folds;
|
|
450
|
+
this.lineHeightsVersion++;
|
|
370
451
|
}
|
|
371
|
-
if (
|
|
452
|
+
if (config.replacements) {
|
|
453
|
+
this.replacements = config.replacements;
|
|
454
|
+
this.lineHeightsVersion++;
|
|
455
|
+
}
|
|
456
|
+
if (measureAgain || (config.textChanged && !config.changes?.length)) {
|
|
457
|
+
this.lineHeightsVersion++;
|
|
372
458
|
this.geometryCache.clear();
|
|
373
459
|
this.measuredRows.clear();
|
|
374
460
|
this.width = 0;
|
|
461
|
+
} else {
|
|
462
|
+
if (config.changes?.length) {
|
|
463
|
+
this.retainGeometry(config.changes);
|
|
464
|
+
this.lineHeightsVersion++;
|
|
465
|
+
}
|
|
466
|
+
if (config.inlays !== undefined) {
|
|
467
|
+
if (!config.changes?.length) {
|
|
468
|
+
for (const line of this.inlaysByLine.keys()) {
|
|
469
|
+
this.geometryCache.delete(line);
|
|
470
|
+
this.measuredRows.delete(line);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const inlay of config.inlays) {
|
|
474
|
+
const line = this.document.positionAt(inlay.at).line;
|
|
475
|
+
this.geometryCache.delete(line);
|
|
476
|
+
this.measuredRows.delete(line);
|
|
477
|
+
}
|
|
478
|
+
this.lineHeightsVersion++;
|
|
479
|
+
}
|
|
480
|
+
this.width = 0;
|
|
481
|
+
if (this.wrapWidth === null) {
|
|
482
|
+
for (const geometry of this.geometryCache.values()) {
|
|
483
|
+
this.width = Math.max(this.width, geometry.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
375
486
|
}
|
|
376
487
|
this.rebuild();
|
|
377
488
|
}
|
|
378
489
|
|
|
490
|
+
private retainGeometry(batches: readonly (readonly DocumentChange[])[]): void {
|
|
491
|
+
const map = lineChangeMapper(batches);
|
|
492
|
+
const remap = (bounds: { start: number; end: number; next: number }): { line: number; start: number; end: number; next: number } | null => {
|
|
493
|
+
const next = map({ start: bounds.start, end: Math.max(bounds.end, bounds.next - 1) });
|
|
494
|
+
if (next === null) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const delta = next.start - bounds.start;
|
|
498
|
+
return { start: next.start, end: bounds.end + delta, next: bounds.next + delta, line: this.document.positionAt(next.start).line };
|
|
499
|
+
};
|
|
500
|
+
const geometries = new Map<number, LineGeometry>();
|
|
501
|
+
for (const geometry of this.geometryCache.values()) {
|
|
502
|
+
const next = remap(geometry);
|
|
503
|
+
if (next !== null && geometry.inlays.length === 0) {
|
|
504
|
+
geometries.set(next.line, next.start === geometry.start ? geometry : { ...geometry, start: next.start, end: next.end, next: next.next });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const measured = new Map<number, { start: number; end: number; next: number; subRows: number }>();
|
|
508
|
+
for (const [line, bounds] of this.measuredRows) {
|
|
509
|
+
const next = remap(bounds);
|
|
510
|
+
if (next !== null && !this.inlaysByLine.has(line)) {
|
|
511
|
+
measured.set(next.line, { start: next.start, end: next.end, next: next.next, subRows: bounds.subRows });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
this.geometryCache = geometries;
|
|
515
|
+
this.measuredRows = measured;
|
|
516
|
+
}
|
|
517
|
+
|
|
379
518
|
private rowCountOf(line: number, length: number): number {
|
|
380
519
|
const limit = this.wrapWidth;
|
|
381
520
|
if (limit === null) {
|
|
@@ -383,7 +522,7 @@ export class EditorLayout {
|
|
|
383
522
|
}
|
|
384
523
|
const measured = this.measuredRows.get(line);
|
|
385
524
|
if (measured !== undefined) {
|
|
386
|
-
return measured;
|
|
525
|
+
return measured.subRows;
|
|
387
526
|
}
|
|
388
527
|
return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
|
|
389
528
|
}
|
|
@@ -412,6 +551,15 @@ export class EditorLayout {
|
|
|
412
551
|
folds.set(fold.startLine, fold);
|
|
413
552
|
}
|
|
414
553
|
}
|
|
554
|
+
const replaced = new Map<number, { replacement: BlockReplacement; endLine: number }>();
|
|
555
|
+
for (const replacement of this.replacements) {
|
|
556
|
+
const startLine = this.document.positionAt(replacement.from).line;
|
|
557
|
+
const endLine = Math.max(startLine, this.document.positionAt(replacement.to).line);
|
|
558
|
+
// A fold the person collapsed shows its own placeholder, so folding a replaced comment keeps working.
|
|
559
|
+
if (!folds.has(startLine) && !replaced.has(startLine)) {
|
|
560
|
+
replaced.set(startLine, { replacement, endLine });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
415
563
|
const count = this.document.getLineCount();
|
|
416
564
|
this.rows = [];
|
|
417
565
|
this.lineRows = new Array<number>(count);
|
|
@@ -424,13 +572,44 @@ export class EditorLayout {
|
|
|
424
572
|
line,
|
|
425
573
|
widget,
|
|
426
574
|
top: 0,
|
|
427
|
-
height: widget.lens
|
|
575
|
+
height: widget.lens
|
|
576
|
+
? this.metrics.lineHeight
|
|
577
|
+
: widget.fixed
|
|
578
|
+
? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
579
|
+
: (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
428
580
|
});
|
|
429
581
|
};
|
|
430
582
|
for (let line = 0; line < count; line++) {
|
|
431
583
|
for (const block of above.get(line) ?? []) {
|
|
432
584
|
addBlock(block, line);
|
|
433
585
|
}
|
|
586
|
+
const standIn = replaced.get(line);
|
|
587
|
+
if (standIn !== undefined) {
|
|
588
|
+
const last = Math.min(count - 1, standIn.endLine);
|
|
589
|
+
const key = `replaced:${standIn.replacement.id}`;
|
|
590
|
+
const index = this.rows.length;
|
|
591
|
+
this.rows.push({
|
|
592
|
+
key,
|
|
593
|
+
kind: 'text',
|
|
594
|
+
index,
|
|
595
|
+
line,
|
|
596
|
+
lastLine: last,
|
|
597
|
+
tail: null,
|
|
598
|
+
head: null,
|
|
599
|
+
subRows: 1,
|
|
600
|
+
top: 0,
|
|
601
|
+
height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
|
|
602
|
+
replacement: standIn.replacement
|
|
603
|
+
});
|
|
604
|
+
for (let covered = line; covered <= last; covered++) {
|
|
605
|
+
this.lineRows[covered] = index;
|
|
606
|
+
}
|
|
607
|
+
for (const block of below.get(last) ?? []) {
|
|
608
|
+
addBlock(block, last);
|
|
609
|
+
}
|
|
610
|
+
line = last;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
434
613
|
const fold = folds.get(line);
|
|
435
614
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
436
615
|
const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
|
|
@@ -480,7 +659,7 @@ export class EditorLayout {
|
|
|
480
659
|
setMeasuredHeight(key: string, height: number): boolean {
|
|
481
660
|
const row = this.rows.find((candidate) => candidate.key === key);
|
|
482
661
|
const next = Math.max(1, Math.round(height));
|
|
483
|
-
if (!row || !Number.isFinite(next) || row.height === next) {
|
|
662
|
+
if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
|
|
484
663
|
return false;
|
|
485
664
|
}
|
|
486
665
|
this.measuredHeights.set(key, next);
|
|
@@ -493,18 +672,27 @@ export class EditorLayout {
|
|
|
493
672
|
syncRows(rows: readonly LayoutRow[]): boolean {
|
|
494
673
|
let changed = false;
|
|
495
674
|
for (const row of rows) {
|
|
496
|
-
if (row.kind !== 'text') {
|
|
675
|
+
if (row.kind !== 'text' || row.replacement !== undefined) {
|
|
497
676
|
continue;
|
|
498
677
|
}
|
|
499
|
-
const
|
|
678
|
+
const geometry = this.geometry(row);
|
|
679
|
+
const subRows = geometry.subRows;
|
|
680
|
+
if (row.head === null && this.wrapWidth !== null) {
|
|
681
|
+
if (!this.measuredRows.has(row.line) && this.measuredRows.size >= GEOMETRY_CACHE_SIZE) {
|
|
682
|
+
this.measuredRows.delete(this.measuredRows.keys().next().value!);
|
|
683
|
+
}
|
|
684
|
+
this.measuredRows.set(row.line, { start: geometry.start, end: geometry.end, next: geometry.next, subRows });
|
|
685
|
+
} else {
|
|
686
|
+
this.measuredRows.delete(row.line);
|
|
687
|
+
}
|
|
500
688
|
if (subRows !== row.subRows) {
|
|
501
|
-
this.measuredRows.set(row.line, subRows);
|
|
502
689
|
row.subRows = subRows;
|
|
503
690
|
row.height = subRows * this.metrics.lineHeight;
|
|
504
691
|
changed = true;
|
|
505
692
|
}
|
|
506
693
|
}
|
|
507
694
|
if (changed) {
|
|
695
|
+
this.lineHeightsVersion++;
|
|
508
696
|
this.reflow();
|
|
509
697
|
}
|
|
510
698
|
return changed;
|
|
@@ -535,7 +723,7 @@ export class EditorLayout {
|
|
|
535
723
|
while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
|
|
536
724
|
stop++;
|
|
537
725
|
}
|
|
538
|
-
return TEXT_PADDING + geometry
|
|
726
|
+
return TEXT_PADDING + xOfStop(geometry, Math.min(stop, stopCount(geometry) - 1));
|
|
539
727
|
}
|
|
540
728
|
|
|
541
729
|
rowForLine(line: number): LayoutRow {
|
|
@@ -591,11 +779,11 @@ export class EditorLayout {
|
|
|
591
779
|
}
|
|
592
780
|
|
|
593
781
|
private stopOf(geometry: LineGeometry, offset: number): number {
|
|
594
|
-
return
|
|
782
|
+
return stopAtOffset(geometry, Math.max(0, offset - geometry.start));
|
|
595
783
|
}
|
|
596
784
|
|
|
597
785
|
xAt(geometry: LineGeometry, offset: number, affinity: Affinity = 'after'): number {
|
|
598
|
-
return TEXT_PADDING + geometry
|
|
786
|
+
return TEXT_PADDING + xOfStop(geometry, this.stopOf(geometry, offset), affinity);
|
|
599
787
|
}
|
|
600
788
|
|
|
601
789
|
/*
|
|
@@ -610,20 +798,20 @@ export class EditorLayout {
|
|
|
610
798
|
}
|
|
611
799
|
}
|
|
612
800
|
const first = geometry.rowStarts[subRow] ?? 0;
|
|
613
|
-
const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : geometry
|
|
801
|
+
const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : stopCount(geometry) - 1;
|
|
614
802
|
const wrapped = subRow + 1 < geometry.rowStarts.length;
|
|
615
803
|
let best = first;
|
|
616
|
-
let distance = Math.abs(local - geometry
|
|
804
|
+
let distance = Math.abs(local - xOfStop(geometry, first));
|
|
617
805
|
// The stop that starts the next visual line stands in for the end of this one.
|
|
618
806
|
for (let stop = first + 1; stop <= last; stop++) {
|
|
619
|
-
const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : geometry
|
|
807
|
+
const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : xOfStop(geometry, stop);
|
|
620
808
|
const candidate = Math.abs(local - edge);
|
|
621
809
|
if (candidate < distance) {
|
|
622
810
|
distance = candidate;
|
|
623
811
|
best = stop;
|
|
624
812
|
}
|
|
625
813
|
}
|
|
626
|
-
return { offset: geometry.start + geometry
|
|
814
|
+
return { offset: geometry.start + offsetOfStop(geometry, best), rowEnd: wrapped && best === last };
|
|
627
815
|
}
|
|
628
816
|
|
|
629
817
|
/* With `rowEnd`, an offset a wrap starts a visual line at is drawn at the end of the line before it. */
|
|
@@ -635,7 +823,7 @@ export class EditorLayout {
|
|
|
635
823
|
}
|
|
636
824
|
const geometry = this.geometry(row);
|
|
637
825
|
const stop = this.stopOf(geometry, offset);
|
|
638
|
-
const subRow = geometry
|
|
826
|
+
const subRow = rowOfStop(geometry, stop);
|
|
639
827
|
if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
|
|
640
828
|
return {
|
|
641
829
|
x: TEXT_PADDING + geometry.rowEnds[subRow - 1]!,
|
|
@@ -644,7 +832,12 @@ export class EditorLayout {
|
|
|
644
832
|
height: this.metrics.lineHeight
|
|
645
833
|
};
|
|
646
834
|
}
|
|
647
|
-
return {
|
|
835
|
+
return {
|
|
836
|
+
x: TEXT_PADDING + xOfStop(geometry, stop, affinity),
|
|
837
|
+
y: row.top + subRow * this.metrics.lineHeight,
|
|
838
|
+
width: 1,
|
|
839
|
+
height: this.metrics.lineHeight
|
|
840
|
+
};
|
|
648
841
|
}
|
|
649
842
|
|
|
650
843
|
/* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
|
|
@@ -671,10 +864,10 @@ export class EditorLayout {
|
|
|
671
864
|
const geometry = this.geometry(row);
|
|
672
865
|
const lastLine = this.document.getLine(row.lastLine);
|
|
673
866
|
const hidden = position.line > row.line;
|
|
674
|
-
const subRow = hidden ? geometry.subRows - 1 : geometry
|
|
867
|
+
const subRow = hidden ? geometry.subRows - 1 : rowOfStop(geometry, this.stopOf(geometry, offset));
|
|
675
868
|
const last = subRow + 1 >= geometry.subRows;
|
|
676
|
-
const start = line.start + geometry
|
|
677
|
-
const end = last ? lastLine.end : line.start + geometry
|
|
869
|
+
const start = line.start + offsetOfStop(geometry, geometry.rowStarts[subRow]!);
|
|
870
|
+
const end = last ? lastLine.end : line.start + offsetOfStop(geometry, geometry.rowStarts[subRow + 1]!);
|
|
678
871
|
return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
|
|
679
872
|
}
|
|
680
873
|
|
|
@@ -693,21 +886,26 @@ export class EditorLayout {
|
|
|
693
886
|
}
|
|
694
887
|
|
|
695
888
|
/* The boxes a range is drawn in, one per visual line it touches, among the rows given. */
|
|
696
|
-
rectangles(from: number, to: number, rows: readonly LayoutRow[]): LayoutRect[] {
|
|
889
|
+
rectangles(from: number, to: number, rows: readonly LayoutRow[], window?: VerticalWindow): LayoutRect[] {
|
|
697
890
|
const result: LayoutRect[] = [];
|
|
698
891
|
const lineHeight = this.metrics.lineHeight;
|
|
699
892
|
for (const row of rows) {
|
|
700
|
-
if (
|
|
893
|
+
if (
|
|
894
|
+
row.kind !== 'text' ||
|
|
895
|
+
row.replacement !== undefined ||
|
|
896
|
+
(window !== undefined && (row.top >= window.bottom || row.top + row.height <= window.top))
|
|
897
|
+
) {
|
|
701
898
|
continue;
|
|
702
899
|
}
|
|
703
900
|
const geometry = this.geometry(row);
|
|
704
901
|
if (from === to || from > geometry.end || to <= geometry.start) {
|
|
705
902
|
continue;
|
|
706
903
|
}
|
|
707
|
-
const lastStop = geometry
|
|
904
|
+
const lastStop = stopCount(geometry) - 1;
|
|
708
905
|
const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
|
|
709
906
|
const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
|
|
710
|
-
|
|
907
|
+
const band = window === undefined ? { first: 0, last: geometry.subRows - 1 } : subRowBand(row.top, geometry.subRows, lineHeight, window);
|
|
908
|
+
for (let subRow = band.first; subRow <= band.last; subRow++) {
|
|
711
909
|
const last = subRow + 1 === geometry.subRows;
|
|
712
910
|
const rowFirst = geometry.rowStarts[subRow]!;
|
|
713
911
|
const rowEnd = last ? lastStop : geometry.rowStarts[subRow + 1]!;
|
|
@@ -717,8 +915,8 @@ export class EditorLayout {
|
|
|
717
915
|
if (first >= stop && !newline) {
|
|
718
916
|
continue;
|
|
719
917
|
}
|
|
720
|
-
const startX = first === fromStop ? geometry
|
|
721
|
-
const endX = stop < rowEnd || last ? geometry
|
|
918
|
+
const startX = first === fromStop ? xOfStop(geometry, first, 'after') : xOfStop(geometry, first);
|
|
919
|
+
const endX = stop < rowEnd || last ? xOfStop(geometry, stop) : geometry.rowEnds[subRow]!;
|
|
722
920
|
result.push({
|
|
723
921
|
x: TEXT_PADDING + startX,
|
|
724
922
|
y: row.top + subRow * lineHeight,
|
|
@@ -731,8 +929,9 @@ export class EditorLayout {
|
|
|
731
929
|
}
|
|
732
930
|
|
|
733
931
|
/*
|
|
734
|
-
* The offset a visual line above or below, `distance` pixels away, at an x. Widgets
|
|
735
|
-
*
|
|
932
|
+
* The offset a visual line above or below, `distance` pixels away, at an x. Widgets and the rows of
|
|
933
|
+
* replaced lines, unless the host made them enterable, are walked over, and the document's ends stop
|
|
934
|
+
* the caret on the first or the last offset.
|
|
736
935
|
*/
|
|
737
936
|
verticalOffset(offset: number, direction: -1 | 1, x: number, distance = this.metrics.lineHeight): number {
|
|
738
937
|
const caret = this.caret(offset);
|
|
@@ -741,10 +940,10 @@ export class EditorLayout {
|
|
|
741
940
|
return 0;
|
|
742
941
|
}
|
|
743
942
|
let row = this.rowAt(y);
|
|
744
|
-
while (row.kind === 'block') {
|
|
943
|
+
while (row.kind === 'block' || (row.replacement !== undefined && row.replacement.enterable !== true)) {
|
|
745
944
|
const next = this.rows[row.index + direction];
|
|
746
945
|
if (!next) {
|
|
747
|
-
return
|
|
946
|
+
return direction < 0 ? 0 : this.document.getLine(this.document.getLineCount() - 1).end;
|
|
748
947
|
}
|
|
749
948
|
row = next;
|
|
750
949
|
y = direction < 0 ? row.top + row.height - 1 : row.top;
|
|
@@ -762,8 +961,8 @@ export class EditorLayout {
|
|
|
762
961
|
const geometry = this.geometry(row);
|
|
763
962
|
const stop = this.stopOf(geometry, offset);
|
|
764
963
|
const next = stop + direction;
|
|
765
|
-
if (next >= 0 && next < geometry
|
|
766
|
-
return geometry.start + geometry
|
|
964
|
+
if (next >= 0 && next < stopCount(geometry) && offset >= geometry.start && offset <= geometry.end) {
|
|
965
|
+
return geometry.start + offsetOfStop(geometry, next);
|
|
767
966
|
}
|
|
768
967
|
}
|
|
769
968
|
for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {
|