@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/dist/view.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { offsetOfStop, xOfStop } from './geometry-stops.js';
|
|
2
|
+
import { visibleBidi } from './bidi.js';
|
|
1
3
|
import { indentationColumn, replacementText, scanBrackets } from '@adecore/editor-core';
|
|
4
|
+
import { createSearchClient } from './search-client.js';
|
|
2
5
|
import { FindController } from './find.js';
|
|
3
|
-
import { EditorLayout, RIGHT_PADDING } from './layout.js';
|
|
6
|
+
import { EditorLayout, RIGHT_PADDING, subRowBand } from './layout.js';
|
|
4
7
|
import { createMetrics, readEditorFont } from './metrics.js';
|
|
5
8
|
import { AttributionRuns, colorValue, tintValue } from './attribution.js';
|
|
6
9
|
import { renderCodeBlock } from './code-block.js';
|
|
7
10
|
import { emit, subscribe } from './listeners.js';
|
|
8
|
-
import { mapOffset } from './offsets.js';
|
|
11
|
+
import { mapOffset, offsetMapper } from './offsets.js';
|
|
9
12
|
import { mapTrackedRange } from './tracked-range.js';
|
|
10
13
|
import { scrollPosition } from './scroll.js';
|
|
11
14
|
import { easeOut, scrollDuration } from './scroll-animation.js';
|
|
@@ -17,16 +20,16 @@ import { TokenCache } from './tokens.js';
|
|
|
17
20
|
function sameRect(left, right) {
|
|
18
21
|
return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
|
|
19
22
|
}
|
|
20
|
-
function mapRange(range,
|
|
21
|
-
return { ...range, from:
|
|
23
|
+
function mapRange(range, map) {
|
|
24
|
+
return { ...range, from: map(range.from), to: map(range.to) };
|
|
22
25
|
}
|
|
23
|
-
function mapFoldHints(hints,
|
|
26
|
+
function mapFoldHints(hints, map) {
|
|
24
27
|
return {
|
|
25
|
-
symbols: hints.symbols?.map((hint) => mapRange(hint,
|
|
28
|
+
symbols: hints.symbols?.map((hint) => mapRange(hint, map)) ?? [],
|
|
26
29
|
ranges: hints.ranges?.map((hint) => ({
|
|
27
|
-
...mapRange(hint,
|
|
28
|
-
...(hint.head === undefined ? {} : { head:
|
|
29
|
-
...(hint.tail === undefined ? {} : { tail:
|
|
30
|
+
...mapRange(hint, map),
|
|
31
|
+
...(hint.head === undefined ? {} : { head: map(hint.head) }),
|
|
32
|
+
...(hint.tail === undefined ? {} : { tail: map(hint.tail) })
|
|
30
33
|
})) ?? []
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -82,6 +85,7 @@ export class EditorView {
|
|
|
82
85
|
preview;
|
|
83
86
|
signs;
|
|
84
87
|
notice;
|
|
88
|
+
live;
|
|
85
89
|
sticky;
|
|
86
90
|
overview;
|
|
87
91
|
tickTip;
|
|
@@ -93,10 +97,17 @@ export class EditorView {
|
|
|
93
97
|
findListeners = new Set();
|
|
94
98
|
scopeListeners = new Set();
|
|
95
99
|
viewListeners = new Set();
|
|
100
|
+
lineHeightListeners = new Set();
|
|
101
|
+
announcedLineHeights = -1;
|
|
96
102
|
gutterActionListeners = new Set();
|
|
97
103
|
gutterAction = null;
|
|
98
104
|
gutterMarkerListeners = new Set();
|
|
99
105
|
gutterMarkers = new Map();
|
|
106
|
+
lineTokenProvider = null;
|
|
107
|
+
lineNumberProvider = null;
|
|
108
|
+
lineNumberLabels = [];
|
|
109
|
+
lineNumberRevision = -1;
|
|
110
|
+
lineNumberWidth = 0;
|
|
100
111
|
semantic = [];
|
|
101
112
|
scopeColors = null;
|
|
102
113
|
semanticVersion = 0;
|
|
@@ -110,8 +121,9 @@ export class EditorView {
|
|
|
110
121
|
outline = new Outline();
|
|
111
122
|
stickyEntries = [];
|
|
112
123
|
scopeKey = '';
|
|
113
|
-
|
|
124
|
+
changeLayers = new Map();
|
|
114
125
|
changeVersion = 0;
|
|
126
|
+
changeCache = null;
|
|
115
127
|
attribution = new AttributionRuns();
|
|
116
128
|
highlights = new AttributionRuns();
|
|
117
129
|
remoteCarets = [];
|
|
@@ -137,9 +149,14 @@ export class EditorView {
|
|
|
137
149
|
frameGeometry = null;
|
|
138
150
|
disposed = false;
|
|
139
151
|
inlays = [];
|
|
152
|
+
inlayHoverListeners = new Set();
|
|
153
|
+
inlayHover = null;
|
|
154
|
+
inlayLink = null;
|
|
155
|
+
inlayLinkElement = null;
|
|
140
156
|
/* The host's widgets by owner, so one owner setting its rows never takes another's away. */
|
|
141
157
|
blocks = new Map();
|
|
142
158
|
lenses = [];
|
|
159
|
+
replacements = new Map();
|
|
143
160
|
occurrences = [];
|
|
144
161
|
replacePreview = null;
|
|
145
162
|
dropCaret = null;
|
|
@@ -166,6 +183,8 @@ export class EditorView {
|
|
|
166
183
|
foldTimer;
|
|
167
184
|
colorTimer;
|
|
168
185
|
noticeTimer;
|
|
186
|
+
liveTimer;
|
|
187
|
+
lineLabelProvider = null;
|
|
169
188
|
bracketCache;
|
|
170
189
|
wrapWidth = null;
|
|
171
190
|
/* The offset a click past the end of a wrapped visual line chose, which its caret is drawn at the end of that line for. */
|
|
@@ -220,6 +239,8 @@ export class EditorView {
|
|
|
220
239
|
for (const name of ['autocapitalize', 'autocomplete', 'autocorrect']) {
|
|
221
240
|
this.input.setAttribute(name, 'off');
|
|
222
241
|
}
|
|
242
|
+
this.input.setAttribute('enterkeyhint', 'enter');
|
|
243
|
+
this.input.setAttribute('inputmode', 'text');
|
|
223
244
|
this.input.setAttribute('aria-multiline', 'true');
|
|
224
245
|
this.sticky.setAttribute('aria-hidden', 'true');
|
|
225
246
|
this.sticky.hidden = true;
|
|
@@ -230,13 +251,30 @@ export class EditorView {
|
|
|
230
251
|
this.notice = make('div', 'se-notice');
|
|
231
252
|
this.notice.setAttribute('role', 'status');
|
|
232
253
|
this.notice.hidden = true;
|
|
233
|
-
this.
|
|
254
|
+
this.live = make('div', 'se-live');
|
|
255
|
+
this.live.setAttribute('role', 'status');
|
|
256
|
+
this.live.setAttribute('aria-live', 'polite');
|
|
257
|
+
this.live.setAttribute('aria-atomic', 'true');
|
|
258
|
+
this.root.append(this.viewport, this.overview, this.tickTip, this.input, this.notice, this.live);
|
|
234
259
|
this.wireOverview();
|
|
235
260
|
this.wireAttribution();
|
|
236
261
|
container.append(this.root);
|
|
237
262
|
this.font = readEditorFont(this.root);
|
|
238
263
|
this.layout = new EditorLayout(model, createMetrics(this.font, settings.tabSize, document));
|
|
239
|
-
this.find = new FindController(model
|
|
264
|
+
this.find = new FindController(model, {
|
|
265
|
+
createClient: () => (settings.searchWorker === undefined ? null : createSearchClient({ workerFactory: settings.searchWorker })),
|
|
266
|
+
changed: (reveal) => {
|
|
267
|
+
if (this.disposed) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.announceFind();
|
|
271
|
+
if (reveal) {
|
|
272
|
+
this.revealFind();
|
|
273
|
+
}
|
|
274
|
+
this.paintPreview();
|
|
275
|
+
this.requestRender();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
240
278
|
this.tokens = new TokenCache({ getLineCount: () => model.getLineCount(), getLineText: (line) => model.getLine(line).text });
|
|
241
279
|
const ResizeObserverClass = document.defaultView?.ResizeObserver;
|
|
242
280
|
this.blockObserver = ResizeObserverClass
|
|
@@ -272,6 +310,10 @@ export class EditorView {
|
|
|
272
310
|
this.layout.configure({ metrics: createMetrics(this.font, this.settings.tabSize, this.document) });
|
|
273
311
|
this.render();
|
|
274
312
|
}
|
|
313
|
+
/* Words the gutter draws changed, not a metric: the lines keep their layout. */
|
|
314
|
+
repaint() {
|
|
315
|
+
this.render();
|
|
316
|
+
}
|
|
275
317
|
refreshFont() {
|
|
276
318
|
this.font = readEditorFont(this.root);
|
|
277
319
|
this.applySettings();
|
|
@@ -281,8 +323,33 @@ export class EditorView {
|
|
|
281
323
|
this.colorAhead();
|
|
282
324
|
this.render();
|
|
283
325
|
}
|
|
326
|
+
grammarChanged() {
|
|
327
|
+
if (!this.disposed && this.lineTokenProvider === null && this.tokens.revalidate()) {
|
|
328
|
+
this.colorAhead(true);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
setLineTokens(tokens) {
|
|
332
|
+
this.lineTokenProvider = tokens;
|
|
333
|
+
clearTimeout(this.colorTimer);
|
|
334
|
+
this.semanticLines.clear();
|
|
335
|
+
this.layoutVersion++;
|
|
336
|
+
this.render();
|
|
337
|
+
}
|
|
338
|
+
setLineNumbers(labels) {
|
|
339
|
+
this.lineNumberProvider = labels;
|
|
340
|
+
this.lineNumberRevision = -1;
|
|
341
|
+
this.stickyEntries = [];
|
|
342
|
+
this.render();
|
|
343
|
+
}
|
|
344
|
+
lineNumberLabel(line) {
|
|
345
|
+
return this.lineNumberProvider === null ? String(line + 1) : (this.lineNumberLabels[line] ?? null);
|
|
346
|
+
}
|
|
284
347
|
setInlays(inlays) {
|
|
285
|
-
this.inlays = inlays.map((inlay) => ({
|
|
348
|
+
this.inlays = inlays.map((inlay) => ({
|
|
349
|
+
...inlay,
|
|
350
|
+
text: inlay.text.replace(/[\r\n]/g, ' '),
|
|
351
|
+
...(inlay.parts === undefined ? {} : { parts: inlay.parts.map((part) => part.replace(/[\r\n]/g, ' ')) })
|
|
352
|
+
}));
|
|
286
353
|
this.configureText();
|
|
287
354
|
}
|
|
288
355
|
setBlockWidgets(owner, blocks) {
|
|
@@ -295,6 +362,23 @@ export class EditorView {
|
|
|
295
362
|
this.anchorScroll(() => this.layout.configure({ blocks: this.allBlocks() }));
|
|
296
363
|
this.render();
|
|
297
364
|
}
|
|
365
|
+
/* Lines the host draws as a row of its own, by owner; where two owners claim the same line the owner that sorts first keeps it. */
|
|
366
|
+
setReplacements(owner, replacements) {
|
|
367
|
+
if (replacements.length === 0 && !this.replacements.has(owner)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (replacements.length === 0) {
|
|
371
|
+
this.replacements.delete(owner);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
this.replacements.set(owner, [...replacements]);
|
|
375
|
+
}
|
|
376
|
+
this.anchorScroll(() => this.layout.configure({ replacements: this.allReplacements() }));
|
|
377
|
+
this.render();
|
|
378
|
+
}
|
|
379
|
+
allReplacements() {
|
|
380
|
+
return [...this.replacements.keys()].sort().flatMap((owner) => this.replacements.get(owner));
|
|
381
|
+
}
|
|
298
382
|
/* The quiet rows above declarations: kept apart from the host's widgets, so setting one kind never takes the other away. */
|
|
299
383
|
setLenses(lenses) {
|
|
300
384
|
this.lenses = [...lenses];
|
|
@@ -334,7 +418,7 @@ export class EditorView {
|
|
|
334
418
|
}
|
|
335
419
|
/* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
|
|
336
420
|
styledTokensOf(line) {
|
|
337
|
-
const base = this.tokens.tokensOf(line);
|
|
421
|
+
const base = this.lineTokenProvider === null ? this.tokens.tokensOf(line) : (this.lineTokenProvider(line) ?? null);
|
|
338
422
|
if (base === null || this.semantic.length === 0 || this.scopeColors === null) {
|
|
339
423
|
return base;
|
|
340
424
|
}
|
|
@@ -342,7 +426,7 @@ export class EditorView {
|
|
|
342
426
|
if (spans === undefined) {
|
|
343
427
|
return base;
|
|
344
428
|
}
|
|
345
|
-
const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}`).join('|');
|
|
429
|
+
const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}:${span.within ?? ''}`).join('|');
|
|
346
430
|
const cached = this.semanticLines.get(line);
|
|
347
431
|
if (cached?.base === base && cached.signature === signature) {
|
|
348
432
|
return cached.result;
|
|
@@ -373,7 +457,7 @@ export class EditorView {
|
|
|
373
457
|
continue;
|
|
374
458
|
}
|
|
375
459
|
const list = lines.get(line) ?? [];
|
|
376
|
-
list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle });
|
|
460
|
+
list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle, within: token.within });
|
|
377
461
|
lines.set(line, list);
|
|
378
462
|
}
|
|
379
463
|
for (const list of lines.values()) {
|
|
@@ -400,6 +484,41 @@ export class EditorView {
|
|
|
400
484
|
this.notice.hidden = true;
|
|
401
485
|
}, NOTICE_MS);
|
|
402
486
|
}
|
|
487
|
+
foldName(line, collapsed) {
|
|
488
|
+
const template = (collapsed ? this.settings.messages.unfold : this.settings.messages.fold) ??
|
|
489
|
+
(collapsed ? 'Unfold lines {start} to {end}' : 'Fold lines {start} to {end}');
|
|
490
|
+
const end = this.foldAt(line)?.endLine ?? line;
|
|
491
|
+
return template.replace('{start}', String(line + 1)).replace('{end}', String(end + 1));
|
|
492
|
+
}
|
|
493
|
+
/* A new child each time, since a region only speaks what is added to it and a repeated sentence would otherwise stay silent. */
|
|
494
|
+
announce(message) {
|
|
495
|
+
const sentence = this.document.createElement('span');
|
|
496
|
+
sentence.textContent = message;
|
|
497
|
+
this.live.replaceChildren(sentence);
|
|
498
|
+
clearTimeout(this.liveTimer);
|
|
499
|
+
this.liveTimer = setTimeout(() => this.live.replaceChildren(), NOTICE_MS);
|
|
500
|
+
}
|
|
501
|
+
setInputPopup(popup) {
|
|
502
|
+
if (popup === null) {
|
|
503
|
+
for (const name of ['aria-expanded', 'aria-controls', 'aria-autocomplete', 'aria-activedescendant']) {
|
|
504
|
+
this.input.removeAttribute(name);
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.input.setAttribute('aria-expanded', 'true');
|
|
509
|
+
this.input.setAttribute('aria-autocomplete', 'list');
|
|
510
|
+
this.input.setAttribute('aria-controls', popup.controls);
|
|
511
|
+
if (popup.activeDescendant === undefined) {
|
|
512
|
+
this.input.removeAttribute('aria-activedescendant');
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
this.input.setAttribute('aria-activedescendant', popup.activeDescendant);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
setLineLabels(labels) {
|
|
519
|
+
this.lineLabelProvider = labels;
|
|
520
|
+
this.render();
|
|
521
|
+
}
|
|
403
522
|
get gutterElement() {
|
|
404
523
|
return this.gutter;
|
|
405
524
|
}
|
|
@@ -412,6 +531,9 @@ export class EditorView {
|
|
|
412
531
|
get focused() {
|
|
413
532
|
return this.document.activeElement === this.input;
|
|
414
533
|
}
|
|
534
|
+
get contentHeight() {
|
|
535
|
+
return this.layout.height + Math.max(0, (this.viewport.offsetHeight ?? 0) - (this.viewport.clientHeight ?? 0));
|
|
536
|
+
}
|
|
415
537
|
get viewportHeight() {
|
|
416
538
|
return this.clientHeight || DEFAULT_HEIGHT;
|
|
417
539
|
}
|
|
@@ -461,7 +583,14 @@ export class EditorView {
|
|
|
461
583
|
this.outline.edited(batches);
|
|
462
584
|
this.changeVersion++;
|
|
463
585
|
this.tokens.edited(batches, (offset) => this.model.positionAt(offset).line);
|
|
464
|
-
this.layout.configure({
|
|
586
|
+
this.layout.configure({
|
|
587
|
+
textChanged: true,
|
|
588
|
+
changes: batches,
|
|
589
|
+
inlays: this.displayedInlays(),
|
|
590
|
+
blocks: this.allBlocks(),
|
|
591
|
+
folds: this.foldStates(),
|
|
592
|
+
replacements: this.allReplacements()
|
|
593
|
+
});
|
|
465
594
|
if (anchor && this.viewport.scrollTop > 0) {
|
|
466
595
|
let offset = anchor.offset;
|
|
467
596
|
for (const changes of batches) {
|
|
@@ -481,28 +610,34 @@ export class EditorView {
|
|
|
481
610
|
}
|
|
482
611
|
/* Moves everything anchored to an offset along with one batch of changes. */
|
|
483
612
|
followEdit(changes) {
|
|
484
|
-
|
|
613
|
+
const map = offsetMapper(changes);
|
|
614
|
+
this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: map(inlay.at) }));
|
|
485
615
|
for (const [owner, blocks] of this.blocks) {
|
|
486
|
-
this.blocks.set(owner, blocks.map((block) => ({ ...block, at:
|
|
616
|
+
this.blocks.set(owner, blocks.map((block) => ({ ...block, at: map(block.at) })));
|
|
487
617
|
}
|
|
488
|
-
this.lenses = this.lenses.map((lens) => ({ ...lens, at:
|
|
489
|
-
|
|
490
|
-
|
|
618
|
+
this.lenses = this.lenses.map((lens) => ({ ...lens, at: map(lens.at) }));
|
|
619
|
+
for (const [owner, entries] of this.replacements) {
|
|
620
|
+
this.replacements.set(owner, entries.map((entry) => ({ ...entry, from: map(entry.from), to: map(entry.to) })));
|
|
621
|
+
}
|
|
622
|
+
this.collapsed = new Set([...this.collapsed].map((anchor) => map(anchor)));
|
|
623
|
+
this.settled = new Set([...this.settled].map((anchor) => map(anchor)));
|
|
491
624
|
if (this.foldHints !== null) {
|
|
492
|
-
this.foldHints = mapFoldHints(this.foldHints,
|
|
625
|
+
this.foldHints = mapFoldHints(this.foldHints, map);
|
|
493
626
|
}
|
|
494
|
-
this.customFolds = this.customFolds.map((fold) => mapRange(fold,
|
|
627
|
+
this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
|
|
495
628
|
this.find.mapBounds(changes);
|
|
496
|
-
|
|
497
|
-
|
|
629
|
+
for (const [layer, marks] of this.changeLayers) {
|
|
630
|
+
this.changeLayers.set(layer, marks.map((mark) => mapRange(mark, map)));
|
|
631
|
+
}
|
|
632
|
+
this.markers = this.markers.map((marker) => mapRange(marker, map));
|
|
498
633
|
this.mapTracked(changes);
|
|
499
|
-
this.attribution.map(changes);
|
|
500
|
-
this.highlights.map(changes);
|
|
501
|
-
this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at:
|
|
634
|
+
this.attribution.map(changes, map);
|
|
635
|
+
this.highlights.map(changes, map);
|
|
636
|
+
this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: map(caret.at) }));
|
|
502
637
|
for (const [owner, entries] of this.lineActions) {
|
|
503
|
-
this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at:
|
|
638
|
+
this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at: map(entry.at) })));
|
|
504
639
|
}
|
|
505
|
-
this.semantic = this.semantic.map((token) => mapRange(token,
|
|
640
|
+
this.semantic = this.semantic.map((token) => mapRange(token, map));
|
|
506
641
|
}
|
|
507
642
|
mapTracked(changes) {
|
|
508
643
|
for (const entry of this.tracked) {
|
|
@@ -531,13 +666,14 @@ export class EditorView {
|
|
|
531
666
|
}
|
|
532
667
|
/* The ranges follow the text until the next read of the document, so a fold above an edit stays closed meanwhile. */
|
|
533
668
|
mappedFolds(batches) {
|
|
669
|
+
const mappers = batches.map(offsetMapper);
|
|
534
670
|
const mapped = [];
|
|
535
671
|
for (const range of this.foldRanges) {
|
|
536
672
|
let from = range.from;
|
|
537
673
|
let to = range.to;
|
|
538
|
-
for (const
|
|
539
|
-
from =
|
|
540
|
-
to =
|
|
674
|
+
for (const map of mappers) {
|
|
675
|
+
from = map(from);
|
|
676
|
+
to = map(to);
|
|
541
677
|
}
|
|
542
678
|
const startLine = this.model.positionAt(from).line;
|
|
543
679
|
const endLine = this.model.positionAt(to).line;
|
|
@@ -574,7 +710,7 @@ export class EditorView {
|
|
|
574
710
|
height: rest.length * this.layout.metrics.lineHeight,
|
|
575
711
|
render: (container) => {
|
|
576
712
|
container.classList.add('se-widget', 'se-ghost-rows');
|
|
577
|
-
renderCodeBlock(container, rest.join('\n'), null, this.settings.tabSize, {});
|
|
713
|
+
renderCodeBlock(container, visibleBidi(rest.join('\n')), null, this.settings.tabSize, {});
|
|
578
714
|
}
|
|
579
715
|
}
|
|
580
716
|
]);
|
|
@@ -611,11 +747,14 @@ export class EditorView {
|
|
|
611
747
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
612
748
|
scheduleFolds() {
|
|
613
749
|
clearTimeout(this.foldTimer);
|
|
750
|
+
if (this.settings.folding === false) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
614
753
|
this.foldTimer = setTimeout(() => this.refreshFolds(), FOLD_DELAY_MS);
|
|
615
754
|
}
|
|
616
755
|
/* Reads the folds off the document now, which otherwise waits for a pause in editing. */
|
|
617
756
|
refreshFolds() {
|
|
618
|
-
if (this.disposed) {
|
|
757
|
+
if (this.disposed || this.settings.folding === false) {
|
|
619
758
|
return;
|
|
620
759
|
}
|
|
621
760
|
const ranges = this.model.getLength() > FOLD_LIMIT
|
|
@@ -768,6 +907,9 @@ export class EditorView {
|
|
|
768
907
|
* and closes one of the document's own. A selection inside one line has no line to fold.
|
|
769
908
|
*/
|
|
770
909
|
foldSelection() {
|
|
910
|
+
if (this.settings.folding === false) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
771
913
|
const { anchor, head } = this.model.getPrimary();
|
|
772
914
|
let from = Math.min(anchor, head);
|
|
773
915
|
let to = Math.max(anchor, head);
|
|
@@ -816,6 +958,9 @@ export class EditorView {
|
|
|
816
958
|
* is what a file opens as the first time, and again as a language server names more of them.
|
|
817
959
|
*/
|
|
818
960
|
restoreFolds(state, defaults = []) {
|
|
961
|
+
if (this.settings.folding === false) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
819
964
|
const last = this.model.getLineCount() - 1;
|
|
820
965
|
if (state !== null) {
|
|
821
966
|
this.customFolds = state.custom
|
|
@@ -909,6 +1054,10 @@ export class EditorView {
|
|
|
909
1054
|
/* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
|
|
910
1055
|
ensureVisible(offset) {
|
|
911
1056
|
const line = this.model.positionAt(offset).line;
|
|
1057
|
+
const row = this.layout.rowForLine(line);
|
|
1058
|
+
if (row.kind === 'text') {
|
|
1059
|
+
row.replacement?.onReveal?.();
|
|
1060
|
+
}
|
|
912
1061
|
if (!this.layout.hides(offset)) {
|
|
913
1062
|
return;
|
|
914
1063
|
}
|
|
@@ -932,7 +1081,8 @@ export class EditorView {
|
|
|
932
1081
|
action();
|
|
933
1082
|
const next = this.layout.rows.find((row) => row.key === anchor.key);
|
|
934
1083
|
if (next) {
|
|
935
|
-
|
|
1084
|
+
// Unwrapping can shrink the anchor below its old offset.
|
|
1085
|
+
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
936
1086
|
}
|
|
937
1087
|
}
|
|
938
1088
|
syncWrap() {
|
|
@@ -1094,6 +1244,22 @@ export class EditorView {
|
|
|
1094
1244
|
this.selectionOccurrenceCache = { key, ranges };
|
|
1095
1245
|
return ranges;
|
|
1096
1246
|
}
|
|
1247
|
+
get paintWindow() {
|
|
1248
|
+
return { top: this.scrollTop - this.overscan, bottom: this.scrollTop + this.viewportHeight + this.overscan };
|
|
1249
|
+
}
|
|
1250
|
+
paintRectangles(from, to, rows) {
|
|
1251
|
+
return this.layout.rectangles(from, to, rows, this.paintWindow);
|
|
1252
|
+
}
|
|
1253
|
+
visibleTextBounds(row) {
|
|
1254
|
+
const geometry = this.layout.geometry(row);
|
|
1255
|
+
const band = subRowBand(row.top, geometry.subRows, this.layout.metrics.lineHeight, this.paintWindow);
|
|
1256
|
+
return {
|
|
1257
|
+
from: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.first]),
|
|
1258
|
+
to: band.last + 1 === geometry.subRows
|
|
1259
|
+
? this.model.getLine(row.line).next
|
|
1260
|
+
: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.last + 1])
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1097
1263
|
/* The arrows of the soft wraps in the rows on screen. */
|
|
1098
1264
|
wrapSigns(rows) {
|
|
1099
1265
|
if (!this.layout.wrapping) {
|
|
@@ -1106,14 +1272,15 @@ export class EditorView {
|
|
|
1106
1272
|
continue;
|
|
1107
1273
|
}
|
|
1108
1274
|
const geometry = this.layout.geometry(row);
|
|
1109
|
-
|
|
1275
|
+
const band = subRowBand(row.top, geometry.subRows, lineHeight, this.paintWindow);
|
|
1276
|
+
for (let subRow = band.first; subRow <= band.last; subRow++) {
|
|
1110
1277
|
const y = row.top + subRow * lineHeight;
|
|
1111
1278
|
if (subRow + 1 < geometry.subRows) {
|
|
1112
1279
|
const x = geometry.rowEnds[subRow];
|
|
1113
1280
|
signs.push({ x, y, width: this.viewportWidth - x });
|
|
1114
1281
|
}
|
|
1115
1282
|
if (subRow > 0) {
|
|
1116
|
-
signs.push({ x: 0, y, width: geometry
|
|
1283
|
+
signs.push({ x: 0, y, width: xOfStop(geometry, geometry.rowStarts[subRow]) });
|
|
1117
1284
|
}
|
|
1118
1285
|
}
|
|
1119
1286
|
}
|
|
@@ -1276,6 +1443,53 @@ export class EditorView {
|
|
|
1276
1443
|
hoverMoved(event) {
|
|
1277
1444
|
this.content.classList.toggle('se-over-selection', !this.settings.readOnly && event.buttons === 0 && this.selectionAtPoint(event.clientX, event.clientY) !== null);
|
|
1278
1445
|
this.setHover(event.buttons === 0 ? this.characterAtPoint(event.clientX, event.clientY, event.target) : null);
|
|
1446
|
+
this.setInlayHover(event.buttons === 0 ? event.target : null);
|
|
1447
|
+
}
|
|
1448
|
+
/* The hint, and the part of its label, an event happened on, or null anywhere else. */
|
|
1449
|
+
inlayAt(target) {
|
|
1450
|
+
const element = target?.closest?.('.se-inlay');
|
|
1451
|
+
if (!element?.dataset.inlayId) {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
const part = target.closest('[data-part]');
|
|
1455
|
+
return { id: element.dataset.inlayId, part: part === null ? null : Number(part.dataset.part), element: part ?? element };
|
|
1456
|
+
}
|
|
1457
|
+
/* Says which hint the pointer is on, or that it left them all. */
|
|
1458
|
+
setInlayHover(target) {
|
|
1459
|
+
const found = this.inlayAt(target);
|
|
1460
|
+
if (found?.id === this.inlayHover?.id && found?.part === this.inlayHover?.part) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
this.inlayHover = found === null ? null : { id: found.id, part: found.part };
|
|
1464
|
+
emit(this.inlayHoverListeners, found === null ? null : { id: found.id, part: found.part, rect: found.element.getBoundingClientRect() });
|
|
1465
|
+
}
|
|
1466
|
+
onInlayHover(listener) {
|
|
1467
|
+
return subscribe(this.inlayHoverListeners, listener);
|
|
1468
|
+
}
|
|
1469
|
+
/* Draws a hint or one of its parts as a link, which only the pill on screen can show. The target outlives a repaint of its line. */
|
|
1470
|
+
setInlayLink(target) {
|
|
1471
|
+
this.inlayLink = target === null ? null : { id: target.id, part: target.part };
|
|
1472
|
+
this.applyInlayLink();
|
|
1473
|
+
}
|
|
1474
|
+
applyInlayLink() {
|
|
1475
|
+
const target = this.inlayLink;
|
|
1476
|
+
if (target === null && this.inlayLinkElement === null) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
let element;
|
|
1480
|
+
if (target !== null) {
|
|
1481
|
+
const pill = Array.from(this.content.querySelectorAll('[data-inlay-id]')).find((candidate) => candidate.dataset.inlayId === target.id);
|
|
1482
|
+
element =
|
|
1483
|
+
target.part === null
|
|
1484
|
+
? pill
|
|
1485
|
+
: (Array.from(pill?.querySelectorAll('[data-part]') ?? []).find((part) => part.dataset.part === String(target.part)) ?? pill);
|
|
1486
|
+
}
|
|
1487
|
+
if (element === this.inlayLinkElement) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
this.inlayLinkElement?.classList.remove('se-inlay-link');
|
|
1491
|
+
element?.classList.add('se-inlay-link');
|
|
1492
|
+
this.inlayLinkElement = element ?? null;
|
|
1279
1493
|
}
|
|
1280
1494
|
setHover(offset) {
|
|
1281
1495
|
if (offset === this.hoverOffset) {
|
|
@@ -1284,6 +1498,27 @@ export class EditorView {
|
|
|
1284
1498
|
this.hoverOffset = offset;
|
|
1285
1499
|
emit(this.hoverListeners, offset);
|
|
1286
1500
|
}
|
|
1501
|
+
getLineHeight(line) {
|
|
1502
|
+
if (!Number.isInteger(line) || line < 0 || line >= this.model.getLineCount() || this.layout.isHidden(line)) {
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
return this.layout.rowForLine(line).height;
|
|
1506
|
+
}
|
|
1507
|
+
onLineHeights(listener) {
|
|
1508
|
+
return subscribe(this.lineHeightListeners, listener);
|
|
1509
|
+
}
|
|
1510
|
+
setScrollTop(top) {
|
|
1511
|
+
if (!Number.isFinite(top)) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
this.cancelScroll();
|
|
1515
|
+
const next = Math.max(0, top);
|
|
1516
|
+
if (this.viewport.scrollTop === next) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.viewport.scrollTop = next;
|
|
1520
|
+
this.render();
|
|
1521
|
+
}
|
|
1287
1522
|
onViewChange(listener) {
|
|
1288
1523
|
return subscribe(this.viewListeners, listener);
|
|
1289
1524
|
}
|
|
@@ -1386,25 +1621,20 @@ export class EditorView {
|
|
|
1386
1621
|
/* What a regular expression replacement writes for the match the find is on, drawn under that match; null takes it away. */
|
|
1387
1622
|
setReplacePreview(replacement, preserveCase = false) {
|
|
1388
1623
|
this.replacePreview = replacement === null ? null : { text: replacement, preserveCase };
|
|
1624
|
+
this.find.setPreview(replacement !== null);
|
|
1389
1625
|
this.paintPreview();
|
|
1390
1626
|
}
|
|
1391
1627
|
/* Shown only when it says something the replacement text does not, as a regular expression's does. */
|
|
1392
1628
|
paintPreview() {
|
|
1393
|
-
const
|
|
1394
|
-
const query = this.find.activeQuery;
|
|
1629
|
+
const match = this.find.currentMatch;
|
|
1395
1630
|
const wanted = this.replacePreview;
|
|
1396
1631
|
let shown = null;
|
|
1397
|
-
if (wanted !== null &&
|
|
1632
|
+
if (wanted !== null && match?.regex === true) {
|
|
1398
1633
|
try {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
from: mark.from,
|
|
1404
|
-
to: mark.to,
|
|
1405
|
-
maxResults: 1
|
|
1406
|
-
})[0];
|
|
1407
|
-
const text = match === undefined ? wanted.text : replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase);
|
|
1634
|
+
if (wanted.text.length > 10000) {
|
|
1635
|
+
throw new RangeError('The replacement preview is too large.');
|
|
1636
|
+
}
|
|
1637
|
+
const text = replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase, 10000);
|
|
1408
1638
|
shown = text === wanted.text ? null : text;
|
|
1409
1639
|
}
|
|
1410
1640
|
catch {
|
|
@@ -1412,11 +1642,11 @@ export class EditorView {
|
|
|
1412
1642
|
}
|
|
1413
1643
|
}
|
|
1414
1644
|
this.preview.hidden = shown === null;
|
|
1415
|
-
if (shown === null ||
|
|
1645
|
+
if (shown === null || match === null) {
|
|
1416
1646
|
return;
|
|
1417
1647
|
}
|
|
1418
|
-
const start = this.layout.caret(
|
|
1419
|
-
const end = this.layout.caret(
|
|
1648
|
+
const start = this.layout.caret(match.from);
|
|
1649
|
+
const end = this.layout.caret(match.to);
|
|
1420
1650
|
this.preview.textContent = shown;
|
|
1421
1651
|
this.preview.style.left = `${(start.x + end.x) / 2}px`;
|
|
1422
1652
|
this.preview.style.top = `${end.y + end.height}px`;
|
|
@@ -1449,7 +1679,7 @@ export class EditorView {
|
|
|
1449
1679
|
}
|
|
1450
1680
|
/* Colors the lines on and just past the screen, a slice at a time so typing never waits on a grammar. */
|
|
1451
1681
|
colorAhead(immediate = false) {
|
|
1452
|
-
if (this.disposed || !this.tokens.ready) {
|
|
1682
|
+
if (this.disposed || this.lineTokenProvider !== null || !this.tokens.ready) {
|
|
1453
1683
|
return;
|
|
1454
1684
|
}
|
|
1455
1685
|
clearTimeout(this.colorTimer);
|
|
@@ -1465,9 +1695,16 @@ export class EditorView {
|
|
|
1465
1695
|
this.colorAhead();
|
|
1466
1696
|
}, immediate ? 0 : 1);
|
|
1467
1697
|
}
|
|
1468
|
-
/*
|
|
1698
|
+
/* Source numbering can be much larger than the display document's line count. */
|
|
1469
1699
|
syncGutter() {
|
|
1470
|
-
|
|
1700
|
+
if (this.lineNumberProvider !== null && this.lineNumberRevision !== this.model.getRevision()) {
|
|
1701
|
+
const provider = this.lineNumberProvider;
|
|
1702
|
+
this.lineNumberLabels = Array.from({ length: this.model.getLineCount() }, (_, line) => provider(line));
|
|
1703
|
+
this.lineNumberWidth = this.lineNumberLabels.reduce((width, label) => Math.max(width, label?.length ?? 0), 0);
|
|
1704
|
+
this.lineNumberRevision = this.model.getRevision();
|
|
1705
|
+
this.stickyEntries = [];
|
|
1706
|
+
}
|
|
1707
|
+
const digits = this.lineNumberProvider === null ? String(this.model.getLineCount()).length : this.lineNumberWidth;
|
|
1471
1708
|
const width = Math.max(MIN_GUTTER_WIDTH, Math.ceil(digits * this.layout.metrics.charWidth) + 40);
|
|
1472
1709
|
if (width !== this.gutterWidth) {
|
|
1473
1710
|
this.gutterWidth = width;
|
|
@@ -1520,7 +1757,7 @@ export class EditorView {
|
|
|
1520
1757
|
const lastLine = rows.at(-1)?.line ?? 0;
|
|
1521
1758
|
const firstRow = this.layout.rowAt(this.scrollTop);
|
|
1522
1759
|
this.topAnchor = { offset: this.model.getLine(firstRow.line).start, delta: this.scrollTop - firstRow.top };
|
|
1523
|
-
if (lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
|
|
1760
|
+
if (this.lineTokenProvider === null && lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
|
|
1524
1761
|
this.tokens.advance(lastLine, FIRST_PAINT_MS);
|
|
1525
1762
|
}
|
|
1526
1763
|
const paint = {
|
|
@@ -1528,6 +1765,7 @@ export class EditorView {
|
|
|
1528
1765
|
whitespace: this.settings.whitespace,
|
|
1529
1766
|
tokensOf: (line) => this.styledTokensOf(line),
|
|
1530
1767
|
viewportWidth: this.viewportWidth - RIGHT_PADDING,
|
|
1768
|
+
window: this.paintWindow,
|
|
1531
1769
|
hostLeft: this.scrollLeft - this.gutterWidth,
|
|
1532
1770
|
hostWidth: this.viewportWidth + this.gutterWidth,
|
|
1533
1771
|
onUnfold: (line) => this.toggleFold(line, false)
|
|
@@ -1536,6 +1774,7 @@ export class EditorView {
|
|
|
1536
1774
|
rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
|
|
1537
1775
|
this.painter.paint(rows, paint);
|
|
1538
1776
|
}
|
|
1777
|
+
this.applyInlayLink();
|
|
1539
1778
|
this.content.style.height = `${this.layout.height}px`;
|
|
1540
1779
|
this.content.style.width = `${Math.max(this.layout.width, this.viewportWidth)}px`;
|
|
1541
1780
|
this.renderedScroll = { top: this.scrollTop, left: this.scrollLeft };
|
|
@@ -1556,6 +1795,11 @@ export class EditorView {
|
|
|
1556
1795
|
this.frameGeometry = null;
|
|
1557
1796
|
this.rendering = false;
|
|
1558
1797
|
}
|
|
1798
|
+
if (!this.disposed && this.announcedLineHeights !== this.layout.lineHeightsVersion) {
|
|
1799
|
+
this.announcedLineHeights = this.layout.lineHeightsVersion;
|
|
1800
|
+
// Hosts may adjust rows in response; rendering must already have finished.
|
|
1801
|
+
emit(this.lineHeightListeners);
|
|
1802
|
+
}
|
|
1559
1803
|
}
|
|
1560
1804
|
paintDecorations(rows) {
|
|
1561
1805
|
const selections = this.model.getSelections();
|
|
@@ -1570,11 +1814,14 @@ export class EditorView {
|
|
|
1570
1814
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
1571
1815
|
const highlighted = this.highlightedLines(firstLine, lastLine);
|
|
1572
1816
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
1817
|
+
lineNumber: (line) => this.lineNumberLabel(line),
|
|
1573
1818
|
changes: this.changedLines(rows),
|
|
1574
1819
|
attribution: this.attributedLines(firstLine, lastLine),
|
|
1575
1820
|
highlights: highlighted,
|
|
1576
1821
|
activeLines: new Set(selections.map((selection) => this.model.positionAt(selection.head).line)),
|
|
1577
1822
|
foldable,
|
|
1823
|
+
lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
|
|
1824
|
+
foldName: (line, collapsed) => this.foldName(line, collapsed),
|
|
1578
1825
|
action: this.gutterAction,
|
|
1579
1826
|
markers: this.markersByLine()
|
|
1580
1827
|
});
|
|
@@ -1588,7 +1835,7 @@ export class EditorView {
|
|
|
1588
1835
|
if (group.length > 0) {
|
|
1589
1836
|
marks.push({
|
|
1590
1837
|
className: kind === 'write' ? 'se-occurrence se-occurrence-write' : 'se-occurrence',
|
|
1591
|
-
rects: group.flatMap((mark) => this.
|
|
1838
|
+
rects: group.flatMap((mark) => this.paintRectangles(mark.from, mark.to, rows))
|
|
1592
1839
|
});
|
|
1593
1840
|
}
|
|
1594
1841
|
}
|
|
@@ -1604,7 +1851,7 @@ export class EditorView {
|
|
|
1604
1851
|
paintSigns(this.signs, this.wrapSigns(rows), this.layout.metrics.lineHeight);
|
|
1605
1852
|
const alike = this.selectionOccurrences();
|
|
1606
1853
|
if (alike.length > 0) {
|
|
1607
|
-
marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.
|
|
1854
|
+
marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.paintRectangles(range.from, range.to, rows)) });
|
|
1608
1855
|
}
|
|
1609
1856
|
const faded = [];
|
|
1610
1857
|
const struck = [];
|
|
@@ -1612,7 +1859,7 @@ export class EditorView {
|
|
|
1612
1859
|
if (marker.to <= marker.from) {
|
|
1613
1860
|
continue;
|
|
1614
1861
|
}
|
|
1615
|
-
const rects = this.
|
|
1862
|
+
const rects = this.paintRectangles(marker.from, marker.to, rows);
|
|
1616
1863
|
if (marker.severity !== 'hint') {
|
|
1617
1864
|
marks.push({ className: `se-squiggle se-squiggle-${marker.severity}`, rects });
|
|
1618
1865
|
}
|
|
@@ -1625,11 +1872,21 @@ export class EditorView {
|
|
|
1625
1872
|
}
|
|
1626
1873
|
const current = this.find.currentMark;
|
|
1627
1874
|
const textRows = rows.filter((candidate) => candidate.kind === 'text');
|
|
1628
|
-
const visibleFrom = textRows.length > 0 ? this.
|
|
1629
|
-
const visibleTo = textRows.length > 0 ? this.
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1875
|
+
const visibleFrom = textRows.length > 0 ? this.visibleTextBounds(textRows[0]).from : 0;
|
|
1876
|
+
const visibleTo = textRows.length > 0 ? this.visibleTextBounds(textRows.at(-1)).to : 0;
|
|
1877
|
+
const found = this.find.matchesBetween(visibleFrom, visibleTo);
|
|
1878
|
+
const spans = current !== null &&
|
|
1879
|
+
!found.some((mark) => mark.from === current.from && mark.to === current.to) &&
|
|
1880
|
+
current.to >= visibleFrom &&
|
|
1881
|
+
current.from <= visibleTo
|
|
1882
|
+
? [...found, current]
|
|
1883
|
+
: found;
|
|
1884
|
+
for (const mark of spans) {
|
|
1885
|
+
if (mark.to > mark.from) {
|
|
1886
|
+
marks.push({
|
|
1887
|
+
className: mark.from === current?.from && mark.to === current.to ? 'se-find-current' : 'se-find-match',
|
|
1888
|
+
rects: this.paintRectangles(mark.from, mark.to, rows)
|
|
1889
|
+
});
|
|
1633
1890
|
}
|
|
1634
1891
|
}
|
|
1635
1892
|
const seen = new Set();
|
|
@@ -1640,25 +1897,25 @@ export class EditorView {
|
|
|
1640
1897
|
}
|
|
1641
1898
|
if (bracket.mate === undefined) {
|
|
1642
1899
|
if (this.unmatchedBracket(bracket.at)) {
|
|
1643
|
-
marks.push({ className: 'se-bracket-unmatched', rects: this.
|
|
1900
|
+
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(bracket.at, bracket.at + 1, rows) });
|
|
1644
1901
|
}
|
|
1645
1902
|
continue;
|
|
1646
1903
|
}
|
|
1647
1904
|
for (const offset of [bracket.at, bracket.mate]) {
|
|
1648
1905
|
if (!seen.has(offset)) {
|
|
1649
1906
|
seen.add(offset);
|
|
1650
|
-
marks.push({ className: 'se-bracket-match', rects: this.
|
|
1907
|
+
marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
|
|
1651
1908
|
}
|
|
1652
1909
|
}
|
|
1653
1910
|
}
|
|
1654
1911
|
const selected = selections.flatMap((selection) => selection.anchor === selection.head
|
|
1655
1912
|
? []
|
|
1656
|
-
: this.
|
|
1913
|
+
: this.paintRectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows));
|
|
1657
1914
|
paintOverlays(this.overlays, { currentLine, selections: selected, focused, marks });
|
|
1658
1915
|
paintOver(this.over, [
|
|
1659
1916
|
{ className: 'se-faded', rects: faded },
|
|
1660
1917
|
{ className: 'se-struck', rects: struck },
|
|
1661
|
-
{ className: 'se-link', rects: this.link === null ? [] : this.
|
|
1918
|
+
{ className: 'se-link', rects: this.link === null ? [] : this.paintRectangles(this.link.from, this.link.to, rows) }
|
|
1662
1919
|
]);
|
|
1663
1920
|
const top = this.scrollTop;
|
|
1664
1921
|
const caretRects = focused
|
|
@@ -1680,36 +1937,81 @@ export class EditorView {
|
|
|
1680
1937
|
this.input.style.height = `${this.layout.metrics.lineHeight}px`;
|
|
1681
1938
|
this.root.dataset.carets = String(selections.length);
|
|
1682
1939
|
}
|
|
1683
|
-
/*
|
|
1684
|
-
|
|
1940
|
+
/*
|
|
1941
|
+
* The lines the host marked in a layer, which stay on their lines through edits until the layer is set again. Layers
|
|
1942
|
+
* draw together; where two mark the same line the layer that was set first after being empty sits below the later one.
|
|
1943
|
+
*/
|
|
1944
|
+
setChangeMarks(marks, layer = '') {
|
|
1685
1945
|
const last = this.model.getLineCount();
|
|
1686
1946
|
const clamp = (line) => Math.min(last, Math.max(1, Math.trunc(line) || 1)) - 1;
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1947
|
+
if (marks.length === 0) {
|
|
1948
|
+
this.changeLayers.delete(layer);
|
|
1949
|
+
}
|
|
1950
|
+
else {
|
|
1951
|
+
this.changeLayers.set(layer, marks.map((mark) => {
|
|
1952
|
+
const from = this.model.getLine(clamp(mark.startLine)).start;
|
|
1953
|
+
return {
|
|
1954
|
+
kind: mark.kind,
|
|
1955
|
+
from,
|
|
1956
|
+
to: mark.kind === 'deleted' ? from : this.model.getLine(Math.max(clamp(mark.startLine), clamp(mark.endLine))).end
|
|
1957
|
+
};
|
|
1958
|
+
}));
|
|
1959
|
+
}
|
|
1691
1960
|
this.changeVersion++;
|
|
1692
1961
|
this.render();
|
|
1693
1962
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1963
|
+
/*
|
|
1964
|
+
* How every marked line differs. The layer that last became non-empty wins a shared line for every kind; inside one
|
|
1965
|
+
* layer a removal never covers a line that was added or changed, as its mark sits on the line after the removed text.
|
|
1966
|
+
*/
|
|
1967
|
+
resolvedChanges() {
|
|
1968
|
+
if (this.changeCache?.version === this.changeVersion) {
|
|
1969
|
+
return this.changeCache;
|
|
1970
|
+
}
|
|
1971
|
+
const lines = new Map();
|
|
1972
|
+
for (const marks of this.changeLayers.values()) {
|
|
1973
|
+
const layer = new Map();
|
|
1974
|
+
for (const mark of marks) {
|
|
1975
|
+
const start = this.model.positionAt(mark.from).line;
|
|
1976
|
+
const end = mark.kind === 'deleted' ? start : this.model.positionAt(Math.max(mark.from, mark.to)).line;
|
|
1977
|
+
for (let line = start; line <= end; line++) {
|
|
1978
|
+
if (mark.kind !== 'deleted' || !layer.has(line)) {
|
|
1979
|
+
layer.set(line, mark.kind);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
for (const [line, kind] of layer) {
|
|
1984
|
+
lines.set(line, kind);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
const runs = [];
|
|
1988
|
+
for (const line of [...lines.keys()].sort((left, right) => left - right)) {
|
|
1989
|
+
const kind = lines.get(line);
|
|
1990
|
+
const run = runs.at(-1);
|
|
1991
|
+
if (run !== undefined && kind !== 'deleted' && run.kind === kind && run.end === line - 1) {
|
|
1992
|
+
run.end = line;
|
|
1993
|
+
}
|
|
1994
|
+
else {
|
|
1995
|
+
runs.push({ kind, start: line, end: line });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
this.changeCache = { version: this.changeVersion, lines, runs };
|
|
1999
|
+
return this.changeCache;
|
|
1697
2000
|
}
|
|
1698
|
-
/* How each line on screen differs
|
|
2001
|
+
/* How each line on screen differs. */
|
|
1699
2002
|
changedLines(rows) {
|
|
1700
|
-
const lines =
|
|
2003
|
+
const { lines } = this.resolvedChanges();
|
|
1701
2004
|
const first = rows[0]?.line ?? 0;
|
|
1702
2005
|
const lastRow = rows.at(-1);
|
|
1703
2006
|
const last = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
}
|
|
2007
|
+
const visible = new Map();
|
|
2008
|
+
for (let line = first; line <= last; line++) {
|
|
2009
|
+
const kind = lines.get(line);
|
|
2010
|
+
if (kind !== undefined) {
|
|
2011
|
+
visible.set(line, kind);
|
|
1710
2012
|
}
|
|
1711
2013
|
}
|
|
1712
|
-
return
|
|
2014
|
+
return visible;
|
|
1713
2015
|
}
|
|
1714
2016
|
/* The carets of agents, each at an offset that follows its text through edits until the host sets them again. */
|
|
1715
2017
|
setRemoteCarets(carets) {
|
|
@@ -1809,6 +2111,7 @@ export class EditorView {
|
|
|
1809
2111
|
highlightMarks(rows, lines) {
|
|
1810
2112
|
const byBackground = new Map();
|
|
1811
2113
|
const width = Math.max(this.layout.width, this.viewportWidth);
|
|
2114
|
+
const emphasisByColor = new Map();
|
|
1812
2115
|
for (const row of rows) {
|
|
1813
2116
|
const highlight = row.kind === 'text' ? lines.get(row.line) : undefined;
|
|
1814
2117
|
if (highlight !== undefined) {
|
|
@@ -1816,13 +2119,25 @@ export class EditorView {
|
|
|
1816
2119
|
const rects = byBackground.get(background) ?? [];
|
|
1817
2120
|
rects.push({ x: 0, y: row.top, width, height: row.height });
|
|
1818
2121
|
byBackground.set(background, rects);
|
|
2122
|
+
if (row.kind === 'text' && highlight.emphasis !== undefined) {
|
|
2123
|
+
const bounds = this.model.getLine(row.line);
|
|
2124
|
+
const emphasisBackground = `color-mix(in srgb, ${colorValue(highlight.color)} 22%, transparent)`;
|
|
2125
|
+
const emphasized = emphasisByColor.get(emphasisBackground) ?? [];
|
|
2126
|
+
for (const range of highlight.emphasis) {
|
|
2127
|
+
emphasized.push(...this.paintRectangles(Math.max(bounds.start, range.from), Math.min(bounds.end, range.to), [row]));
|
|
2128
|
+
}
|
|
2129
|
+
emphasisByColor.set(emphasisBackground, emphasized);
|
|
2130
|
+
}
|
|
1819
2131
|
}
|
|
1820
2132
|
}
|
|
1821
|
-
return [
|
|
2133
|
+
return [
|
|
2134
|
+
...[...byBackground].map(([background, rects]) => ({ className: 'se-line-highlight', rects, background })),
|
|
2135
|
+
...[...emphasisByColor].map(([background, rects]) => ({ className: 'se-line-emphasis', rects, background }))
|
|
2136
|
+
];
|
|
1822
2137
|
}
|
|
1823
2138
|
/* Code that is not in the document, drawn in the editor's own face and colors. */
|
|
1824
2139
|
renderCode(container, text, options) {
|
|
1825
|
-
renderCodeBlock(container, text, this.tokens.tokenizeText(text.split(/\r?\n/)), this.settings.tabSize, options);
|
|
2140
|
+
renderCodeBlock(container, text, options.tokens === undefined ? this.tokens.tokenizeText(text.split(/\r?\n/)) : options.tokens, this.settings.tabSize, options);
|
|
1826
2141
|
}
|
|
1827
2142
|
attributedLines(first, last) {
|
|
1828
2143
|
return this.attribution.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
|
|
@@ -1926,9 +2241,8 @@ export class EditorView {
|
|
|
1926
2241
|
const row = this.layout.rowForLine(line);
|
|
1927
2242
|
return row.top + row.height;
|
|
1928
2243
|
};
|
|
1929
|
-
for (const
|
|
1930
|
-
|
|
1931
|
-
spans.push({ kind: mark.kind, top: rowTop(start), bottom: mark.kind === 'deleted' ? rowTop(start) : rowBottom(end) });
|
|
2244
|
+
for (const { kind, start, end } of this.resolvedChanges().runs) {
|
|
2245
|
+
spans.push({ kind, top: rowTop(start), bottom: kind === 'deleted' ? rowTop(start) : rowBottom(end) });
|
|
1932
2246
|
}
|
|
1933
2247
|
for (const marker of this.markers) {
|
|
1934
2248
|
if (marker.severity !== 'hint' && marker.to > marker.from) {
|
|
@@ -1980,6 +2294,9 @@ export class EditorView {
|
|
|
1980
2294
|
return Math.min(STICKY_MAX_LINES, Math.floor(this.viewportHeight / this.layout.metrics.lineHeight / STICKY_ROW_SHARE));
|
|
1981
2295
|
}
|
|
1982
2296
|
stickyAt(scrollTop) {
|
|
2297
|
+
if (this.settings.stickyScroll === false) {
|
|
2298
|
+
return [];
|
|
2299
|
+
}
|
|
1983
2300
|
const { layout } = this;
|
|
1984
2301
|
const bottomOf = (line) => {
|
|
1985
2302
|
const row = layout.rowForLine(line);
|
|
@@ -2003,12 +2320,17 @@ export class EditorView {
|
|
|
2003
2320
|
const placements = this.scrollTop <= 0 ? [] : this.stickyAt(this.scrollTop);
|
|
2004
2321
|
const entries = placements.map((placement) => {
|
|
2005
2322
|
const row = this.layout.rowForLine(placement.block.startLine);
|
|
2006
|
-
return {
|
|
2323
|
+
return {
|
|
2324
|
+
line: placement.block.startLine,
|
|
2325
|
+
label: this.lineNumberLabel(placement.block.startLine),
|
|
2326
|
+
geometry: this.layout.geometry(row),
|
|
2327
|
+
tokens: this.styledTokensOf(placement.block.startLine)
|
|
2328
|
+
};
|
|
2007
2329
|
});
|
|
2008
2330
|
const changed = entries.length !== this.stickyEntries.length ||
|
|
2009
2331
|
entries.some((entry, index) => {
|
|
2010
2332
|
const before = this.stickyEntries[index];
|
|
2011
|
-
return entry.line !== before.line || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
|
|
2333
|
+
return entry.line !== before.line || entry.label !== before.label || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
|
|
2012
2334
|
});
|
|
2013
2335
|
this.sticky.hidden = entries.length === 0;
|
|
2014
2336
|
this.sticky.style.setProperty('--se-scroll-left', `${this.scrollLeft}px`);
|
|
@@ -2047,7 +2369,11 @@ export class EditorView {
|
|
|
2047
2369
|
setGutterMarkers(owner, markers) {
|
|
2048
2370
|
const before = this.gutterMarkers.get(owner) ?? [];
|
|
2049
2371
|
if (before.length === markers.length &&
|
|
2050
|
-
before.every((marker, index) => marker.id === markers[index].id &&
|
|
2372
|
+
before.every((marker, index) => marker.id === markers[index].id &&
|
|
2373
|
+
marker.line === markers[index].line &&
|
|
2374
|
+
marker.label === markers[index].label &&
|
|
2375
|
+
marker.icon === markers[index].icon &&
|
|
2376
|
+
marker.pressed === markers[index].pressed)) {
|
|
2051
2377
|
return;
|
|
2052
2378
|
}
|
|
2053
2379
|
if (markers.length === 0) {
|
|
@@ -2075,7 +2401,7 @@ export class EditorView {
|
|
|
2075
2401
|
for (const owner of [...this.gutterMarkers.keys()].sort()) {
|
|
2076
2402
|
for (const marker of this.gutterMarkers.get(owner)) {
|
|
2077
2403
|
if (!byLine.has(marker.line)) {
|
|
2078
|
-
byLine.set(marker.line,
|
|
2404
|
+
byLine.set(marker.line, marker);
|
|
2079
2405
|
}
|
|
2080
2406
|
}
|
|
2081
2407
|
}
|
|
@@ -2100,12 +2426,14 @@ export class EditorView {
|
|
|
2100
2426
|
}
|
|
2101
2427
|
this.disposed = true;
|
|
2102
2428
|
this.cancelScroll();
|
|
2429
|
+
this.find.dispose();
|
|
2103
2430
|
this.subscription.dispose();
|
|
2104
2431
|
this.resizeObserver?.disconnect();
|
|
2105
2432
|
this.blockObserver?.disconnect();
|
|
2106
2433
|
clearTimeout(this.foldTimer);
|
|
2107
2434
|
clearTimeout(this.colorTimer);
|
|
2108
2435
|
clearTimeout(this.noticeTimer);
|
|
2436
|
+
clearTimeout(this.liveTimer);
|
|
2109
2437
|
clearTimeout(this.caretTimer);
|
|
2110
2438
|
if (this.frame !== undefined) {
|
|
2111
2439
|
this.document.defaultView?.cancelAnimationFrame?.(this.frame);
|
|
@@ -2113,7 +2441,9 @@ export class EditorView {
|
|
|
2113
2441
|
this.findListeners.clear();
|
|
2114
2442
|
this.scopeListeners.clear();
|
|
2115
2443
|
this.viewListeners.clear();
|
|
2444
|
+
this.lineHeightListeners.clear();
|
|
2116
2445
|
this.hoverListeners.clear();
|
|
2446
|
+
this.inlayHoverListeners.clear();
|
|
2117
2447
|
this.gutterActionListeners.clear();
|
|
2118
2448
|
this.gutterMarkerListeners.clear();
|
|
2119
2449
|
this.gutterMarkers.clear();
|