@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/view.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { offsetOfStop, xOfStop } from './geometry-stops.ts';
|
|
2
|
+
import { visibleBidi } from './bidi.ts';
|
|
1
3
|
import {
|
|
2
4
|
type DocumentChange,
|
|
3
5
|
type DocumentModel,
|
|
@@ -11,13 +13,26 @@ import {
|
|
|
11
13
|
type BracketIndex,
|
|
12
14
|
type Selection as EditorSelection
|
|
13
15
|
} from '@adecore/editor-core';
|
|
16
|
+
import { createSearchClient, type EditorSearchClientOptions } from './search-client.ts';
|
|
14
17
|
import { FindController } from './find.ts';
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type BlockReplacement,
|
|
20
|
+
type BlockWidget,
|
|
21
|
+
EditorLayout,
|
|
22
|
+
type FoldState,
|
|
23
|
+
type Inlay,
|
|
24
|
+
type LayoutRect,
|
|
25
|
+
type LayoutRow,
|
|
26
|
+
RIGHT_PADDING,
|
|
27
|
+
type TextRow,
|
|
28
|
+
subRowBand,
|
|
29
|
+
type VerticalWindow
|
|
30
|
+
} from './layout.ts';
|
|
16
31
|
import { createMetrics, type EditorFont, readEditorFont } from './metrics.ts';
|
|
17
32
|
import { type AttributedLines, AttributionRuns, colorValue, tintValue } from './attribution.ts';
|
|
18
33
|
import { renderCodeBlock } from './code-block.ts';
|
|
19
34
|
import { emit, subscribe } from './listeners.ts';
|
|
20
|
-
import { mapOffset } from './offsets.ts';
|
|
35
|
+
import { mapOffset, offsetMapper } from './offsets.ts';
|
|
21
36
|
import { mapTrackedRange } from './tracked-range.ts';
|
|
22
37
|
import { type ScrollKind, scrollPosition } from './scroll.ts';
|
|
23
38
|
import { easeOut, scrollDuration } from './scroll-animation.ts';
|
|
@@ -46,11 +61,15 @@ import type {
|
|
|
46
61
|
EditorFindQuery,
|
|
47
62
|
EditorFindState,
|
|
48
63
|
EditorHighlightKind,
|
|
64
|
+
EditorInputPopup,
|
|
65
|
+
EditorLineHighlight,
|
|
66
|
+
EditorGutterIcon,
|
|
49
67
|
EditorMarkerSeverity,
|
|
50
68
|
EditorMessages,
|
|
51
69
|
EditorRect,
|
|
52
70
|
EditorSmartKeys,
|
|
53
71
|
LineToken,
|
|
72
|
+
LineTokenKind,
|
|
54
73
|
LineTokenizer,
|
|
55
74
|
ScopeColors
|
|
56
75
|
} from './types.ts';
|
|
@@ -59,23 +78,24 @@ function sameRect(left: EditorRect, right: EditorRect): boolean {
|
|
|
59
78
|
return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
function mapRange<Range extends { from: number; to: number }>(range: Range,
|
|
63
|
-
return { ...range, from:
|
|
81
|
+
function mapRange<Range extends { from: number; to: number }>(range: Range, map: (offset: number) => number): Range {
|
|
82
|
+
return { ...range, from: map(range.from), to: map(range.to) };
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
function mapFoldHints(hints: FoldHints,
|
|
85
|
+
function mapFoldHints(hints: FoldHints, map: (offset: number) => number): FoldHints {
|
|
67
86
|
return {
|
|
68
|
-
symbols: hints.symbols?.map((hint) => mapRange(hint,
|
|
87
|
+
symbols: hints.symbols?.map((hint) => mapRange(hint, map)) ?? [],
|
|
69
88
|
ranges:
|
|
70
89
|
hints.ranges?.map((hint) => ({
|
|
71
|
-
...mapRange(hint,
|
|
72
|
-
...(hint.head === undefined ? {} : { head:
|
|
73
|
-
...(hint.tail === undefined ? {} : { tail:
|
|
90
|
+
...mapRange(hint, map),
|
|
91
|
+
...(hint.head === undefined ? {} : { head: map(hint.head) }),
|
|
92
|
+
...(hint.tail === undefined ? {} : { tail: map(hint.tail) })
|
|
74
93
|
})) ?? []
|
|
75
94
|
};
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
export interface ViewSettings {
|
|
98
|
+
searchWorker?: EditorSearchClientOptions['workerFactory'];
|
|
79
99
|
language: string | undefined;
|
|
80
100
|
tabSize: number;
|
|
81
101
|
insertSpaces: boolean;
|
|
@@ -93,6 +113,9 @@ export interface ViewSettings {
|
|
|
93
113
|
rightMargin: number | null;
|
|
94
114
|
/* When the arrow that folds a block shows in the gutter. */
|
|
95
115
|
foldOutline: 'off' | 'hover' | 'always';
|
|
116
|
+
folding?: boolean;
|
|
117
|
+
/* The headers of the blocks scrolled out of sight stay pinned on top; on unless false. */
|
|
118
|
+
stickyScroll?: boolean;
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
/* The lines past the visible ones that are colored ahead of the scroll, and how long one slice of that work may take. */
|
|
@@ -142,9 +165,18 @@ export interface ViewSemanticToken {
|
|
|
142
165
|
from: number;
|
|
143
166
|
to: number;
|
|
144
167
|
scopes: readonly string[];
|
|
168
|
+
within?: LineTokenKind;
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
/* A problem the host marked, anchored to offsets that follow the text through edits. */
|
|
172
|
+
export interface GutterMarker {
|
|
173
|
+
id: string;
|
|
174
|
+
line: number;
|
|
175
|
+
label: string;
|
|
176
|
+
icon?: EditorGutterIcon;
|
|
177
|
+
pressed?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
export interface ViewMarker {
|
|
149
181
|
severity: EditorMarkerSeverity;
|
|
150
182
|
from: number;
|
|
@@ -176,6 +208,7 @@ export class EditorView {
|
|
|
176
208
|
private readonly preview: HTMLElement;
|
|
177
209
|
private readonly signs: HTMLElement;
|
|
178
210
|
private readonly notice: HTMLElement;
|
|
211
|
+
private readonly live: HTMLElement;
|
|
179
212
|
private readonly sticky: HTMLElement;
|
|
180
213
|
private readonly overview: HTMLElement;
|
|
181
214
|
private readonly tickTip: HTMLElement;
|
|
@@ -187,10 +220,17 @@ export class EditorView {
|
|
|
187
220
|
private readonly findListeners = new Set<(state: EditorFindState) => void>();
|
|
188
221
|
private readonly scopeListeners = new Set<(scope: readonly EditorBlock[]) => void>();
|
|
189
222
|
private readonly viewListeners = new Set<() => void>();
|
|
223
|
+
private readonly lineHeightListeners = new Set<() => void>();
|
|
224
|
+
private announcedLineHeights = -1;
|
|
190
225
|
private readonly gutterActionListeners = new Set<(line: number) => void>();
|
|
191
226
|
private gutterAction: { line: number; label: string } | null = null;
|
|
192
227
|
private readonly gutterMarkerListeners = new Set<(id: string) => void>();
|
|
193
|
-
private readonly gutterMarkers = new Map<string, readonly
|
|
228
|
+
private readonly gutterMarkers = new Map<string, readonly GutterMarker[]>();
|
|
229
|
+
private lineTokenProvider: ((line: number) => readonly LineToken[] | null | undefined) | null = null;
|
|
230
|
+
private lineNumberProvider: ((line: number) => string | null) | null = null;
|
|
231
|
+
private lineNumberLabels: (string | null)[] = [];
|
|
232
|
+
private lineNumberRevision = -1;
|
|
233
|
+
private lineNumberWidth = 0;
|
|
194
234
|
private semantic: ViewSemanticToken[] = [];
|
|
195
235
|
private scopeColors: ScopeColors | null = null;
|
|
196
236
|
private semanticVersion = 0;
|
|
@@ -204,8 +244,13 @@ export class EditorView {
|
|
|
204
244
|
private readonly outline = new Outline();
|
|
205
245
|
private stickyEntries: StickyEntry[] = [];
|
|
206
246
|
private scopeKey = '';
|
|
207
|
-
private
|
|
247
|
+
private readonly changeLayers = new Map<string, { kind: EditorChangeKind; from: number; to: number }[]>();
|
|
208
248
|
private changeVersion = 0;
|
|
249
|
+
private changeCache: {
|
|
250
|
+
version: number;
|
|
251
|
+
lines: Map<number, EditorChangeKind>;
|
|
252
|
+
runs: { kind: EditorChangeKind; start: number; end: number }[];
|
|
253
|
+
} | null = null;
|
|
209
254
|
private readonly attribution = new AttributionRuns();
|
|
210
255
|
private readonly highlights = new AttributionRuns();
|
|
211
256
|
private remoteCarets: { id: string; name: string; color: string; at: number }[] = [];
|
|
@@ -231,9 +276,14 @@ export class EditorView {
|
|
|
231
276
|
private frameGeometry: { top: number; left: number; width: number; height: number } | null = null;
|
|
232
277
|
private disposed = false;
|
|
233
278
|
private inlays: Inlay[] = [];
|
|
279
|
+
private readonly inlayHoverListeners = new Set<(target: { id: string; part: number | null; rect: EditorRect } | null) => void>();
|
|
280
|
+
private inlayHover: { id: string; part: number | null } | null = null;
|
|
281
|
+
private inlayLink: { id: string; part: number | null } | null = null;
|
|
282
|
+
private inlayLinkElement: HTMLElement | null = null;
|
|
234
283
|
/* The host's widgets by owner, so one owner setting its rows never takes another's away. */
|
|
235
284
|
private readonly blocks = new Map<string, BlockWidget[]>();
|
|
236
285
|
private lenses: BlockWidget[] = [];
|
|
286
|
+
private readonly replacements = new Map<string, BlockReplacement[]>();
|
|
237
287
|
private occurrences: Occurrence[] = [];
|
|
238
288
|
private replacePreview: { text: string; preserveCase: boolean } | null = null;
|
|
239
289
|
private dropCaret: number | null = null;
|
|
@@ -260,6 +310,8 @@ export class EditorView {
|
|
|
260
310
|
private foldTimer: ReturnType<typeof setTimeout> | undefined;
|
|
261
311
|
private colorTimer: ReturnType<typeof setTimeout> | undefined;
|
|
262
312
|
private noticeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
313
|
+
private liveTimer: ReturnType<typeof setTimeout> | undefined;
|
|
314
|
+
private lineLabelProvider: ((line: number) => string | null) | null = null;
|
|
263
315
|
private bracketCache: { revision: number; language: string | undefined; index: BracketIndex } | undefined;
|
|
264
316
|
private wrapWidth: number | null = null;
|
|
265
317
|
/* 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. */
|
|
@@ -316,6 +368,8 @@ export class EditorView {
|
|
|
316
368
|
for (const name of ['autocapitalize', 'autocomplete', 'autocorrect']) {
|
|
317
369
|
this.input.setAttribute(name, 'off');
|
|
318
370
|
}
|
|
371
|
+
this.input.setAttribute('enterkeyhint', 'enter');
|
|
372
|
+
this.input.setAttribute('inputmode', 'text');
|
|
319
373
|
this.input.setAttribute('aria-multiline', 'true');
|
|
320
374
|
this.sticky.setAttribute('aria-hidden', 'true');
|
|
321
375
|
this.sticky.hidden = true;
|
|
@@ -326,14 +380,31 @@ export class EditorView {
|
|
|
326
380
|
this.notice = make('div', 'se-notice');
|
|
327
381
|
this.notice.setAttribute('role', 'status');
|
|
328
382
|
this.notice.hidden = true;
|
|
329
|
-
this.
|
|
383
|
+
this.live = make('div', 'se-live');
|
|
384
|
+
this.live.setAttribute('role', 'status');
|
|
385
|
+
this.live.setAttribute('aria-live', 'polite');
|
|
386
|
+
this.live.setAttribute('aria-atomic', 'true');
|
|
387
|
+
this.root.append(this.viewport, this.overview, this.tickTip, this.input, this.notice, this.live);
|
|
330
388
|
this.wireOverview();
|
|
331
389
|
this.wireAttribution();
|
|
332
390
|
container.append(this.root);
|
|
333
391
|
|
|
334
392
|
this.font = readEditorFont(this.root);
|
|
335
393
|
this.layout = new EditorLayout(model, createMetrics(this.font, settings.tabSize, document));
|
|
336
|
-
this.find = new FindController(model
|
|
394
|
+
this.find = new FindController(model, {
|
|
395
|
+
createClient: () => (settings.searchWorker === undefined ? null : createSearchClient({ workerFactory: settings.searchWorker })),
|
|
396
|
+
changed: (reveal) => {
|
|
397
|
+
if (this.disposed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.announceFind();
|
|
401
|
+
if (reveal) {
|
|
402
|
+
this.revealFind();
|
|
403
|
+
}
|
|
404
|
+
this.paintPreview();
|
|
405
|
+
this.requestRender();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
337
408
|
this.tokens = new TokenCache({ getLineCount: () => model.getLineCount(), getLineText: (line) => model.getLine(line).text });
|
|
338
409
|
const ResizeObserverClass = document.defaultView?.ResizeObserver;
|
|
339
410
|
this.blockObserver = ResizeObserverClass
|
|
@@ -371,6 +442,11 @@ export class EditorView {
|
|
|
371
442
|
this.render();
|
|
372
443
|
}
|
|
373
444
|
|
|
445
|
+
/* Words the gutter draws changed, not a metric: the lines keep their layout. */
|
|
446
|
+
repaint(): void {
|
|
447
|
+
this.render();
|
|
448
|
+
}
|
|
449
|
+
|
|
374
450
|
refreshFont(): void {
|
|
375
451
|
this.font = readEditorFont(this.root);
|
|
376
452
|
this.applySettings();
|
|
@@ -382,8 +458,37 @@ export class EditorView {
|
|
|
382
458
|
this.render();
|
|
383
459
|
}
|
|
384
460
|
|
|
461
|
+
grammarChanged(): void {
|
|
462
|
+
if (!this.disposed && this.lineTokenProvider === null && this.tokens.revalidate()) {
|
|
463
|
+
this.colorAhead(true);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void {
|
|
468
|
+
this.lineTokenProvider = tokens;
|
|
469
|
+
clearTimeout(this.colorTimer);
|
|
470
|
+
this.semanticLines.clear();
|
|
471
|
+
this.layoutVersion++;
|
|
472
|
+
this.render();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setLineNumbers(labels: ((line: number) => string | null) | null): void {
|
|
476
|
+
this.lineNumberProvider = labels;
|
|
477
|
+
this.lineNumberRevision = -1;
|
|
478
|
+
this.stickyEntries = [];
|
|
479
|
+
this.render();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private lineNumberLabel(line: number): string | null {
|
|
483
|
+
return this.lineNumberProvider === null ? String(line + 1) : (this.lineNumberLabels[line] ?? null);
|
|
484
|
+
}
|
|
485
|
+
|
|
385
486
|
setInlays(inlays: readonly Inlay[]): void {
|
|
386
|
-
this.inlays = inlays.map((inlay) => ({
|
|
487
|
+
this.inlays = inlays.map((inlay) => ({
|
|
488
|
+
...inlay,
|
|
489
|
+
text: inlay.text.replace(/[\r\n]/g, ' '),
|
|
490
|
+
...(inlay.parts === undefined ? {} : { parts: inlay.parts.map((part) => part.replace(/[\r\n]/g, ' ')) })
|
|
491
|
+
}));
|
|
387
492
|
this.configureText();
|
|
388
493
|
}
|
|
389
494
|
|
|
@@ -397,6 +502,24 @@ export class EditorView {
|
|
|
397
502
|
this.render();
|
|
398
503
|
}
|
|
399
504
|
|
|
505
|
+
/* 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. */
|
|
506
|
+
setReplacements(owner: string, replacements: readonly BlockReplacement[]): void {
|
|
507
|
+
if (replacements.length === 0 && !this.replacements.has(owner)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (replacements.length === 0) {
|
|
511
|
+
this.replacements.delete(owner);
|
|
512
|
+
} else {
|
|
513
|
+
this.replacements.set(owner, [...replacements]);
|
|
514
|
+
}
|
|
515
|
+
this.anchorScroll(() => this.layout.configure({ replacements: this.allReplacements() }));
|
|
516
|
+
this.render();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private allReplacements(): BlockReplacement[] {
|
|
520
|
+
return [...this.replacements.keys()].sort().flatMap((owner) => this.replacements.get(owner)!);
|
|
521
|
+
}
|
|
522
|
+
|
|
400
523
|
/* The quiet rows above declarations: kept apart from the host's widgets, so setting one kind never takes the other away. */
|
|
401
524
|
setLenses(lenses: readonly BlockWidget[]): void {
|
|
402
525
|
this.lenses = [...lenses];
|
|
@@ -442,7 +565,7 @@ export class EditorView {
|
|
|
442
565
|
|
|
443
566
|
/* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
|
|
444
567
|
private styledTokensOf(line: number): readonly LineToken[] | null {
|
|
445
|
-
const base = this.tokens.tokensOf(line);
|
|
568
|
+
const base = this.lineTokenProvider === null ? this.tokens.tokensOf(line) : (this.lineTokenProvider(line) ?? null);
|
|
446
569
|
if (base === null || this.semantic.length === 0 || this.scopeColors === null) {
|
|
447
570
|
return base;
|
|
448
571
|
}
|
|
@@ -450,7 +573,7 @@ export class EditorView {
|
|
|
450
573
|
if (spans === undefined) {
|
|
451
574
|
return base;
|
|
452
575
|
}
|
|
453
|
-
const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}`).join('|');
|
|
576
|
+
const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}:${span.within ?? ''}`).join('|');
|
|
454
577
|
const cached = this.semanticLines.get(line);
|
|
455
578
|
if (cached?.base === base && cached.signature === signature) {
|
|
456
579
|
return cached.result;
|
|
@@ -482,7 +605,7 @@ export class EditorView {
|
|
|
482
605
|
continue;
|
|
483
606
|
}
|
|
484
607
|
const list = lines.get(line) ?? [];
|
|
485
|
-
list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle });
|
|
608
|
+
list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle, within: token.within });
|
|
486
609
|
lines.set(line, list);
|
|
487
610
|
}
|
|
488
611
|
for (const list of lines.values()) {
|
|
@@ -513,6 +636,45 @@ export class EditorView {
|
|
|
513
636
|
}, NOTICE_MS);
|
|
514
637
|
}
|
|
515
638
|
|
|
639
|
+
private foldName(line: number, collapsed: boolean): string {
|
|
640
|
+
const template =
|
|
641
|
+
(collapsed ? this.settings.messages.unfold : this.settings.messages.fold) ??
|
|
642
|
+
(collapsed ? 'Unfold lines {start} to {end}' : 'Fold lines {start} to {end}');
|
|
643
|
+
const end = this.foldAt(line)?.endLine ?? line;
|
|
644
|
+
return template.replace('{start}', String(line + 1)).replace('{end}', String(end + 1));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* A new child each time, since a region only speaks what is added to it and a repeated sentence would otherwise stay silent. */
|
|
648
|
+
announce(message: string): void {
|
|
649
|
+
const sentence = this.document.createElement('span');
|
|
650
|
+
sentence.textContent = message;
|
|
651
|
+
this.live.replaceChildren(sentence);
|
|
652
|
+
clearTimeout(this.liveTimer);
|
|
653
|
+
this.liveTimer = setTimeout(() => this.live.replaceChildren(), NOTICE_MS);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
setInputPopup(popup: EditorInputPopup | null): void {
|
|
657
|
+
if (popup === null) {
|
|
658
|
+
for (const name of ['aria-expanded', 'aria-controls', 'aria-autocomplete', 'aria-activedescendant']) {
|
|
659
|
+
this.input.removeAttribute(name);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.input.setAttribute('aria-expanded', 'true');
|
|
664
|
+
this.input.setAttribute('aria-autocomplete', 'list');
|
|
665
|
+
this.input.setAttribute('aria-controls', popup.controls);
|
|
666
|
+
if (popup.activeDescendant === undefined) {
|
|
667
|
+
this.input.removeAttribute('aria-activedescendant');
|
|
668
|
+
} else {
|
|
669
|
+
this.input.setAttribute('aria-activedescendant', popup.activeDescendant);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
setLineLabels(labels: ((line: number) => string | null) | null): void {
|
|
674
|
+
this.lineLabelProvider = labels;
|
|
675
|
+
this.render();
|
|
676
|
+
}
|
|
677
|
+
|
|
516
678
|
get gutterElement(): HTMLElement {
|
|
517
679
|
return this.gutter;
|
|
518
680
|
}
|
|
@@ -529,6 +691,10 @@ export class EditorView {
|
|
|
529
691
|
return this.document.activeElement === this.input;
|
|
530
692
|
}
|
|
531
693
|
|
|
694
|
+
get contentHeight(): number {
|
|
695
|
+
return this.layout.height + Math.max(0, (this.viewport.offsetHeight ?? 0) - (this.viewport.clientHeight ?? 0));
|
|
696
|
+
}
|
|
697
|
+
|
|
532
698
|
get viewportHeight(): number {
|
|
533
699
|
return this.clientHeight || DEFAULT_HEIGHT;
|
|
534
700
|
}
|
|
@@ -585,7 +751,14 @@ export class EditorView {
|
|
|
585
751
|
this.outline.edited(batches);
|
|
586
752
|
this.changeVersion++;
|
|
587
753
|
this.tokens.edited(batches, (offset) => this.model.positionAt(offset).line);
|
|
588
|
-
this.layout.configure({
|
|
754
|
+
this.layout.configure({
|
|
755
|
+
textChanged: true,
|
|
756
|
+
changes: batches,
|
|
757
|
+
inlays: this.displayedInlays(),
|
|
758
|
+
blocks: this.allBlocks(),
|
|
759
|
+
folds: this.foldStates(),
|
|
760
|
+
replacements: this.allReplacements()
|
|
761
|
+
});
|
|
589
762
|
if (anchor && this.viewport.scrollTop > 0) {
|
|
590
763
|
let offset = anchor.offset;
|
|
591
764
|
for (const changes of batches) {
|
|
@@ -606,34 +779,46 @@ export class EditorView {
|
|
|
606
779
|
|
|
607
780
|
/* Moves everything anchored to an offset along with one batch of changes. */
|
|
608
781
|
private followEdit(changes: readonly DocumentChange[]): void {
|
|
609
|
-
|
|
782
|
+
const map = offsetMapper(changes);
|
|
783
|
+
this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: map(inlay.at) }));
|
|
610
784
|
for (const [owner, blocks] of this.blocks) {
|
|
611
785
|
this.blocks.set(
|
|
612
786
|
owner,
|
|
613
|
-
blocks.map((block) => ({ ...block, at:
|
|
787
|
+
blocks.map((block) => ({ ...block, at: map(block.at) }))
|
|
614
788
|
);
|
|
615
789
|
}
|
|
616
|
-
this.lenses = this.lenses.map((lens) => ({ ...lens, at:
|
|
617
|
-
|
|
618
|
-
|
|
790
|
+
this.lenses = this.lenses.map((lens) => ({ ...lens, at: map(lens.at) }));
|
|
791
|
+
for (const [owner, entries] of this.replacements) {
|
|
792
|
+
this.replacements.set(
|
|
793
|
+
owner,
|
|
794
|
+
entries.map((entry) => ({ ...entry, from: map(entry.from), to: map(entry.to) }))
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
this.collapsed = new Set([...this.collapsed].map((anchor) => map(anchor)));
|
|
798
|
+
this.settled = new Set([...this.settled].map((anchor) => map(anchor)));
|
|
619
799
|
if (this.foldHints !== null) {
|
|
620
|
-
this.foldHints = mapFoldHints(this.foldHints,
|
|
800
|
+
this.foldHints = mapFoldHints(this.foldHints, map);
|
|
621
801
|
}
|
|
622
|
-
this.customFolds = this.customFolds.map((fold) => mapRange(fold,
|
|
802
|
+
this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
|
|
623
803
|
this.find.mapBounds(changes);
|
|
624
|
-
|
|
625
|
-
|
|
804
|
+
for (const [layer, marks] of this.changeLayers) {
|
|
805
|
+
this.changeLayers.set(
|
|
806
|
+
layer,
|
|
807
|
+
marks.map((mark) => mapRange(mark, map))
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
this.markers = this.markers.map((marker) => mapRange(marker, map));
|
|
626
811
|
this.mapTracked(changes);
|
|
627
|
-
this.attribution.map(changes);
|
|
628
|
-
this.highlights.map(changes);
|
|
629
|
-
this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at:
|
|
812
|
+
this.attribution.map(changes, map);
|
|
813
|
+
this.highlights.map(changes, map);
|
|
814
|
+
this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: map(caret.at) }));
|
|
630
815
|
for (const [owner, entries] of this.lineActions) {
|
|
631
816
|
this.lineActions.set(
|
|
632
817
|
owner,
|
|
633
|
-
entries.map((entry) => ({ ...entry, at:
|
|
818
|
+
entries.map((entry) => ({ ...entry, at: map(entry.at) }))
|
|
634
819
|
);
|
|
635
820
|
}
|
|
636
|
-
this.semantic = this.semantic.map((token) => mapRange(token,
|
|
821
|
+
this.semantic = this.semantic.map((token) => mapRange(token, map));
|
|
637
822
|
}
|
|
638
823
|
|
|
639
824
|
private mapTracked(changes: readonly DocumentChange[]): void {
|
|
@@ -664,13 +849,14 @@ export class EditorView {
|
|
|
664
849
|
|
|
665
850
|
/* The ranges follow the text until the next read of the document, so a fold above an edit stays closed meanwhile. */
|
|
666
851
|
private mappedFolds(batches: readonly (readonly DocumentChange[])[]): ViewFold[] {
|
|
852
|
+
const mappers = batches.map(offsetMapper);
|
|
667
853
|
const mapped: ViewFold[] = [];
|
|
668
854
|
for (const range of this.foldRanges) {
|
|
669
855
|
let from = range.from;
|
|
670
856
|
let to = range.to;
|
|
671
|
-
for (const
|
|
672
|
-
from =
|
|
673
|
-
to =
|
|
857
|
+
for (const map of mappers) {
|
|
858
|
+
from = map(from);
|
|
859
|
+
to = map(to);
|
|
674
860
|
}
|
|
675
861
|
const startLine = this.model.positionAt(from).line;
|
|
676
862
|
const endLine = this.model.positionAt(to).line;
|
|
@@ -712,7 +898,7 @@ export class EditorView {
|
|
|
712
898
|
height: rest.length * this.layout.metrics.lineHeight,
|
|
713
899
|
render: (container) => {
|
|
714
900
|
container.classList.add('se-widget', 'se-ghost-rows');
|
|
715
|
-
renderCodeBlock(container, rest.join('\n'), null, this.settings.tabSize, {});
|
|
901
|
+
renderCodeBlock(container, visibleBidi(rest.join('\n')), null, this.settings.tabSize, {});
|
|
716
902
|
}
|
|
717
903
|
}
|
|
718
904
|
]
|
|
@@ -756,12 +942,15 @@ export class EditorView {
|
|
|
756
942
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
757
943
|
private scheduleFolds(): void {
|
|
758
944
|
clearTimeout(this.foldTimer);
|
|
945
|
+
if (this.settings.folding === false) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
759
948
|
this.foldTimer = setTimeout(() => this.refreshFolds(), FOLD_DELAY_MS);
|
|
760
949
|
}
|
|
761
950
|
|
|
762
951
|
/* Reads the folds off the document now, which otherwise waits for a pause in editing. */
|
|
763
952
|
refreshFolds(): void {
|
|
764
|
-
if (this.disposed) {
|
|
953
|
+
if (this.disposed || this.settings.folding === false) {
|
|
765
954
|
return;
|
|
766
955
|
}
|
|
767
956
|
const ranges =
|
|
@@ -934,6 +1123,9 @@ export class EditorView {
|
|
|
934
1123
|
* and closes one of the document's own. A selection inside one line has no line to fold.
|
|
935
1124
|
*/
|
|
936
1125
|
foldSelection(): void {
|
|
1126
|
+
if (this.settings.folding === false) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
937
1129
|
const { anchor, head } = this.model.getPrimary();
|
|
938
1130
|
let from = Math.min(anchor, head);
|
|
939
1131
|
let to = Math.max(anchor, head);
|
|
@@ -985,6 +1177,9 @@ export class EditorView {
|
|
|
985
1177
|
state: { collapsed: readonly { startLine: number; endLine: number }[]; custom: readonly { startLine: number; endLine: number }[] } | null,
|
|
986
1178
|
defaults: readonly FoldRole[] = []
|
|
987
1179
|
): void {
|
|
1180
|
+
if (this.settings.folding === false) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
988
1183
|
const last = this.model.getLineCount() - 1;
|
|
989
1184
|
if (state !== null) {
|
|
990
1185
|
this.customFolds = state.custom
|
|
@@ -1085,6 +1280,10 @@ export class EditorView {
|
|
|
1085
1280
|
/* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
|
|
1086
1281
|
ensureVisible(offset: number): void {
|
|
1087
1282
|
const line = this.model.positionAt(offset).line;
|
|
1283
|
+
const row = this.layout.rowForLine(line);
|
|
1284
|
+
if (row.kind === 'text') {
|
|
1285
|
+
row.replacement?.onReveal?.();
|
|
1286
|
+
}
|
|
1088
1287
|
if (!this.layout.hides(offset)) {
|
|
1089
1288
|
return;
|
|
1090
1289
|
}
|
|
@@ -1109,7 +1308,8 @@ export class EditorView {
|
|
|
1109
1308
|
action();
|
|
1110
1309
|
const next = this.layout.rows.find((row) => row.key === anchor.key);
|
|
1111
1310
|
if (next) {
|
|
1112
|
-
|
|
1311
|
+
// Unwrapping can shrink the anchor below its old offset.
|
|
1312
|
+
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
1113
1313
|
}
|
|
1114
1314
|
}
|
|
1115
1315
|
|
|
@@ -1290,6 +1490,26 @@ export class EditorView {
|
|
|
1290
1490
|
return ranges;
|
|
1291
1491
|
}
|
|
1292
1492
|
|
|
1493
|
+
private get paintWindow(): VerticalWindow {
|
|
1494
|
+
return { top: this.scrollTop - this.overscan, bottom: this.scrollTop + this.viewportHeight + this.overscan };
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private paintRectangles(from: number, to: number, rows: readonly LayoutRow[]): LayoutRect[] {
|
|
1498
|
+
return this.layout.rectangles(from, to, rows, this.paintWindow);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private visibleTextBounds(row: TextRow): { from: number; to: number } {
|
|
1502
|
+
const geometry = this.layout.geometry(row);
|
|
1503
|
+
const band = subRowBand(row.top, geometry.subRows, this.layout.metrics.lineHeight, this.paintWindow);
|
|
1504
|
+
return {
|
|
1505
|
+
from: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.first]!),
|
|
1506
|
+
to:
|
|
1507
|
+
band.last + 1 === geometry.subRows
|
|
1508
|
+
? this.model.getLine(row.line).next
|
|
1509
|
+
: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.last + 1]!)
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1293
1513
|
/* The arrows of the soft wraps in the rows on screen. */
|
|
1294
1514
|
private wrapSigns(rows: readonly LayoutRow[]): WrapSign[] {
|
|
1295
1515
|
if (!this.layout.wrapping) {
|
|
@@ -1302,14 +1522,15 @@ export class EditorView {
|
|
|
1302
1522
|
continue;
|
|
1303
1523
|
}
|
|
1304
1524
|
const geometry = this.layout.geometry(row);
|
|
1305
|
-
|
|
1525
|
+
const band = subRowBand(row.top, geometry.subRows, lineHeight, this.paintWindow);
|
|
1526
|
+
for (let subRow = band.first; subRow <= band.last; subRow++) {
|
|
1306
1527
|
const y = row.top + subRow * lineHeight;
|
|
1307
1528
|
if (subRow + 1 < geometry.subRows) {
|
|
1308
1529
|
const x = geometry.rowEnds[subRow]!;
|
|
1309
1530
|
signs.push({ x, y, width: this.viewportWidth - x });
|
|
1310
1531
|
}
|
|
1311
1532
|
if (subRow > 0) {
|
|
1312
|
-
signs.push({ x: 0, y, width: geometry
|
|
1533
|
+
signs.push({ x: 0, y, width: xOfStop(geometry, geometry.rowStarts[subRow]!) });
|
|
1313
1534
|
}
|
|
1314
1535
|
}
|
|
1315
1536
|
}
|
|
@@ -1494,6 +1715,58 @@ export class EditorView {
|
|
|
1494
1715
|
!this.settings.readOnly && event.buttons === 0 && this.selectionAtPoint(event.clientX, event.clientY) !== null
|
|
1495
1716
|
);
|
|
1496
1717
|
this.setHover(event.buttons === 0 ? this.characterAtPoint(event.clientX, event.clientY, event.target) : null);
|
|
1718
|
+
this.setInlayHover(event.buttons === 0 ? event.target : null);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/* The hint, and the part of its label, an event happened on, or null anywhere else. */
|
|
1722
|
+
inlayAt(target: EventTarget | null): { id: string; part: number | null; element: HTMLElement } | null {
|
|
1723
|
+
const element = (target as HTMLElement | null)?.closest?.<HTMLElement>('.se-inlay');
|
|
1724
|
+
if (!element?.dataset.inlayId) {
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
const part = (target as HTMLElement).closest<HTMLElement>('[data-part]');
|
|
1728
|
+
return { id: element.dataset.inlayId, part: part === null ? null : Number(part.dataset.part), element: part ?? element };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/* Says which hint the pointer is on, or that it left them all. */
|
|
1732
|
+
setInlayHover(target: EventTarget | null): void {
|
|
1733
|
+
const found = this.inlayAt(target);
|
|
1734
|
+
if (found?.id === this.inlayHover?.id && found?.part === this.inlayHover?.part) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
this.inlayHover = found === null ? null : { id: found.id, part: found.part };
|
|
1738
|
+
emit(this.inlayHoverListeners, found === null ? null : { id: found.id, part: found.part, rect: found.element.getBoundingClientRect() });
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
onInlayHover(listener: (target: { id: string; part: number | null; rect: EditorRect } | null) => void): () => void {
|
|
1742
|
+
return subscribe(this.inlayHoverListeners, listener);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/* 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. */
|
|
1746
|
+
setInlayLink(target: { id: string; part: number | null } | null): void {
|
|
1747
|
+
this.inlayLink = target === null ? null : { id: target.id, part: target.part };
|
|
1748
|
+
this.applyInlayLink();
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
private applyInlayLink(): void {
|
|
1752
|
+
const target = this.inlayLink;
|
|
1753
|
+
if (target === null && this.inlayLinkElement === null) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
let element: HTMLElement | undefined;
|
|
1757
|
+
if (target !== null) {
|
|
1758
|
+
const pill = Array.from(this.content.querySelectorAll<HTMLElement>('[data-inlay-id]')).find((candidate) => candidate.dataset.inlayId === target.id);
|
|
1759
|
+
element =
|
|
1760
|
+
target.part === null
|
|
1761
|
+
? pill
|
|
1762
|
+
: (Array.from(pill?.querySelectorAll<HTMLElement>('[data-part]') ?? []).find((part) => part.dataset.part === String(target.part)) ?? pill);
|
|
1763
|
+
}
|
|
1764
|
+
if (element === this.inlayLinkElement) {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
this.inlayLinkElement?.classList.remove('se-inlay-link');
|
|
1768
|
+
element?.classList.add('se-inlay-link');
|
|
1769
|
+
this.inlayLinkElement = element ?? null;
|
|
1497
1770
|
}
|
|
1498
1771
|
|
|
1499
1772
|
setHover(offset: number | null): void {
|
|
@@ -1504,6 +1777,30 @@ export class EditorView {
|
|
|
1504
1777
|
emit(this.hoverListeners, offset);
|
|
1505
1778
|
}
|
|
1506
1779
|
|
|
1780
|
+
getLineHeight(line: number): number {
|
|
1781
|
+
if (!Number.isInteger(line) || line < 0 || line >= this.model.getLineCount() || this.layout.isHidden(line)) {
|
|
1782
|
+
return 0;
|
|
1783
|
+
}
|
|
1784
|
+
return this.layout.rowForLine(line).height;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
onLineHeights(listener: () => void): () => void {
|
|
1788
|
+
return subscribe(this.lineHeightListeners, listener);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
setScrollTop(top: number): void {
|
|
1792
|
+
if (!Number.isFinite(top)) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
this.cancelScroll();
|
|
1796
|
+
const next = Math.max(0, top);
|
|
1797
|
+
if (this.viewport.scrollTop === next) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
this.viewport.scrollTop = next;
|
|
1801
|
+
this.render();
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1507
1804
|
onViewChange(listener: () => void): () => void {
|
|
1508
1805
|
return subscribe(this.viewListeners, listener);
|
|
1509
1806
|
}
|
|
@@ -1618,37 +1915,32 @@ export class EditorView {
|
|
|
1618
1915
|
/* What a regular expression replacement writes for the match the find is on, drawn under that match; null takes it away. */
|
|
1619
1916
|
setReplacePreview(replacement: string | null, preserveCase = false): void {
|
|
1620
1917
|
this.replacePreview = replacement === null ? null : { text: replacement, preserveCase };
|
|
1918
|
+
this.find.setPreview(replacement !== null);
|
|
1621
1919
|
this.paintPreview();
|
|
1622
1920
|
}
|
|
1623
1921
|
|
|
1624
1922
|
/* Shown only when it says something the replacement text does not, as a regular expression's does. */
|
|
1625
1923
|
private paintPreview(): void {
|
|
1626
|
-
const
|
|
1627
|
-
const query = this.find.activeQuery;
|
|
1924
|
+
const match = this.find.currentMatch;
|
|
1628
1925
|
const wanted = this.replacePreview;
|
|
1629
1926
|
let shown: string | null = null;
|
|
1630
|
-
if (wanted !== null &&
|
|
1927
|
+
if (wanted !== null && match?.regex === true) {
|
|
1631
1928
|
try {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
from: mark.from,
|
|
1637
|
-
to: mark.to,
|
|
1638
|
-
maxResults: 1
|
|
1639
|
-
})[0];
|
|
1640
|
-
const text = match === undefined ? wanted.text : replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase);
|
|
1929
|
+
if (wanted.text.length > 10000) {
|
|
1930
|
+
throw new RangeError('The replacement preview is too large.');
|
|
1931
|
+
}
|
|
1932
|
+
const text = replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase, 10000);
|
|
1641
1933
|
shown = text === wanted.text ? null : text;
|
|
1642
1934
|
} catch {
|
|
1643
1935
|
shown = null;
|
|
1644
1936
|
}
|
|
1645
1937
|
}
|
|
1646
1938
|
this.preview.hidden = shown === null;
|
|
1647
|
-
if (shown === null ||
|
|
1939
|
+
if (shown === null || match === null) {
|
|
1648
1940
|
return;
|
|
1649
1941
|
}
|
|
1650
|
-
const start = this.layout.caret(
|
|
1651
|
-
const end = this.layout.caret(
|
|
1942
|
+
const start = this.layout.caret(match.from);
|
|
1943
|
+
const end = this.layout.caret(match.to);
|
|
1652
1944
|
this.preview.textContent = shown;
|
|
1653
1945
|
this.preview.style.left = `${(start.x + end.x) / 2}px`;
|
|
1654
1946
|
this.preview.style.top = `${end.y + end.height}px`;
|
|
@@ -1687,7 +1979,7 @@ export class EditorView {
|
|
|
1687
1979
|
|
|
1688
1980
|
/* Colors the lines on and just past the screen, a slice at a time so typing never waits on a grammar. */
|
|
1689
1981
|
colorAhead(immediate = false): void {
|
|
1690
|
-
if (this.disposed || !this.tokens.ready) {
|
|
1982
|
+
if (this.disposed || this.lineTokenProvider !== null || !this.tokens.ready) {
|
|
1691
1983
|
return;
|
|
1692
1984
|
}
|
|
1693
1985
|
clearTimeout(this.colorTimer);
|
|
@@ -1707,9 +1999,16 @@ export class EditorView {
|
|
|
1707
1999
|
);
|
|
1708
2000
|
}
|
|
1709
2001
|
|
|
1710
|
-
/*
|
|
2002
|
+
/* Source numbering can be much larger than the display document's line count. */
|
|
1711
2003
|
private syncGutter(): void {
|
|
1712
|
-
|
|
2004
|
+
if (this.lineNumberProvider !== null && this.lineNumberRevision !== this.model.getRevision()) {
|
|
2005
|
+
const provider = this.lineNumberProvider;
|
|
2006
|
+
this.lineNumberLabels = Array.from({ length: this.model.getLineCount() }, (_, line) => provider(line));
|
|
2007
|
+
this.lineNumberWidth = this.lineNumberLabels.reduce((width, label) => Math.max(width, label?.length ?? 0), 0);
|
|
2008
|
+
this.lineNumberRevision = this.model.getRevision();
|
|
2009
|
+
this.stickyEntries = [];
|
|
2010
|
+
}
|
|
2011
|
+
const digits = this.lineNumberProvider === null ? String(this.model.getLineCount()).length : this.lineNumberWidth;
|
|
1713
2012
|
const width = Math.max(MIN_GUTTER_WIDTH, Math.ceil(digits * this.layout.metrics.charWidth) + 40);
|
|
1714
2013
|
if (width !== this.gutterWidth) {
|
|
1715
2014
|
this.gutterWidth = width;
|
|
@@ -1765,7 +2064,7 @@ export class EditorView {
|
|
|
1765
2064
|
const lastLine = rows.at(-1)?.line ?? 0;
|
|
1766
2065
|
const firstRow = this.layout.rowAt(this.scrollTop);
|
|
1767
2066
|
this.topAnchor = { offset: this.model.getLine(firstRow.line).start, delta: this.scrollTop - firstRow.top };
|
|
1768
|
-
if (lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
|
|
2067
|
+
if (this.lineTokenProvider === null && lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
|
|
1769
2068
|
this.tokens.advance(lastLine, FIRST_PAINT_MS);
|
|
1770
2069
|
}
|
|
1771
2070
|
const paint = {
|
|
@@ -1773,6 +2072,7 @@ export class EditorView {
|
|
|
1773
2072
|
whitespace: this.settings.whitespace,
|
|
1774
2073
|
tokensOf: (line: number) => this.styledTokensOf(line),
|
|
1775
2074
|
viewportWidth: this.viewportWidth - RIGHT_PADDING,
|
|
2075
|
+
window: this.paintWindow,
|
|
1776
2076
|
hostLeft: this.scrollLeft - this.gutterWidth,
|
|
1777
2077
|
hostWidth: this.viewportWidth + this.gutterWidth,
|
|
1778
2078
|
onUnfold: (line: number) => this.toggleFold(line, false)
|
|
@@ -1781,6 +2081,7 @@ export class EditorView {
|
|
|
1781
2081
|
rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
|
|
1782
2082
|
this.painter.paint(rows, paint);
|
|
1783
2083
|
}
|
|
2084
|
+
this.applyInlayLink();
|
|
1784
2085
|
this.content.style.height = `${this.layout.height}px`;
|
|
1785
2086
|
this.content.style.width = `${Math.max(this.layout.width, this.viewportWidth)}px`;
|
|
1786
2087
|
this.renderedScroll = { top: this.scrollTop, left: this.scrollLeft };
|
|
@@ -1800,6 +2101,11 @@ export class EditorView {
|
|
|
1800
2101
|
this.frameGeometry = null;
|
|
1801
2102
|
this.rendering = false;
|
|
1802
2103
|
}
|
|
2104
|
+
if (!this.disposed && this.announcedLineHeights !== this.layout.lineHeightsVersion) {
|
|
2105
|
+
this.announcedLineHeights = this.layout.lineHeightsVersion;
|
|
2106
|
+
// Hosts may adjust rows in response; rendering must already have finished.
|
|
2107
|
+
emit(this.lineHeightListeners);
|
|
2108
|
+
}
|
|
1803
2109
|
}
|
|
1804
2110
|
|
|
1805
2111
|
private paintDecorations(rows: readonly LayoutRow[]): void {
|
|
@@ -1815,11 +2121,14 @@ export class EditorView {
|
|
|
1815
2121
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
1816
2122
|
const highlighted = this.highlightedLines(firstLine, lastLine);
|
|
1817
2123
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
2124
|
+
lineNumber: (line) => this.lineNumberLabel(line),
|
|
1818
2125
|
changes: this.changedLines(rows),
|
|
1819
2126
|
attribution: this.attributedLines(firstLine, lastLine),
|
|
1820
2127
|
highlights: highlighted,
|
|
1821
2128
|
activeLines: new Set(selections.map((selection) => this.model.positionAt(selection.head).line)),
|
|
1822
2129
|
foldable,
|
|
2130
|
+
lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
|
|
2131
|
+
foldName: (line, collapsed) => this.foldName(line, collapsed),
|
|
1823
2132
|
action: this.gutterAction,
|
|
1824
2133
|
markers: this.markersByLine()
|
|
1825
2134
|
});
|
|
@@ -1834,7 +2143,7 @@ export class EditorView {
|
|
|
1834
2143
|
if (group.length > 0) {
|
|
1835
2144
|
marks.push({
|
|
1836
2145
|
className: kind === 'write' ? 'se-occurrence se-occurrence-write' : 'se-occurrence',
|
|
1837
|
-
rects: group.flatMap((mark) => this.
|
|
2146
|
+
rects: group.flatMap((mark) => this.paintRectangles(mark.from, mark.to, rows))
|
|
1838
2147
|
});
|
|
1839
2148
|
}
|
|
1840
2149
|
}
|
|
@@ -1850,7 +2159,7 @@ export class EditorView {
|
|
|
1850
2159
|
paintSigns(this.signs, this.wrapSigns(rows), this.layout.metrics.lineHeight);
|
|
1851
2160
|
const alike = this.selectionOccurrences();
|
|
1852
2161
|
if (alike.length > 0) {
|
|
1853
|
-
marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.
|
|
2162
|
+
marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.paintRectangles(range.from, range.to, rows)) });
|
|
1854
2163
|
}
|
|
1855
2164
|
const faded: LayoutRect[] = [];
|
|
1856
2165
|
const struck: LayoutRect[] = [];
|
|
@@ -1858,7 +2167,7 @@ export class EditorView {
|
|
|
1858
2167
|
if (marker.to <= marker.from) {
|
|
1859
2168
|
continue;
|
|
1860
2169
|
}
|
|
1861
|
-
const rects = this.
|
|
2170
|
+
const rects = this.paintRectangles(marker.from, marker.to, rows);
|
|
1862
2171
|
if (marker.severity !== 'hint') {
|
|
1863
2172
|
marks.push({ className: `se-squiggle se-squiggle-${marker.severity}`, rects });
|
|
1864
2173
|
}
|
|
@@ -1871,11 +2180,22 @@ export class EditorView {
|
|
|
1871
2180
|
}
|
|
1872
2181
|
const current = this.find.currentMark;
|
|
1873
2182
|
const textRows = rows.filter((candidate) => candidate.kind === 'text');
|
|
1874
|
-
const visibleFrom = textRows.length > 0 ? this.
|
|
1875
|
-
const visibleTo = textRows.length > 0 ? this.
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2183
|
+
const visibleFrom = textRows.length > 0 ? this.visibleTextBounds(textRows[0]!).from : 0;
|
|
2184
|
+
const visibleTo = textRows.length > 0 ? this.visibleTextBounds(textRows.at(-1)!).to : 0;
|
|
2185
|
+
const found = this.find.matchesBetween(visibleFrom, visibleTo);
|
|
2186
|
+
const spans =
|
|
2187
|
+
current !== null &&
|
|
2188
|
+
!found.some((mark) => mark.from === current.from && mark.to === current.to) &&
|
|
2189
|
+
current.to >= visibleFrom &&
|
|
2190
|
+
current.from <= visibleTo
|
|
2191
|
+
? [...found, current]
|
|
2192
|
+
: found;
|
|
2193
|
+
for (const mark of spans) {
|
|
2194
|
+
if (mark.to > mark.from) {
|
|
2195
|
+
marks.push({
|
|
2196
|
+
className: mark.from === current?.from && mark.to === current.to ? 'se-find-current' : 'se-find-match',
|
|
2197
|
+
rects: this.paintRectangles(mark.from, mark.to, rows)
|
|
2198
|
+
});
|
|
1879
2199
|
}
|
|
1880
2200
|
}
|
|
1881
2201
|
const seen = new Set<number>();
|
|
@@ -1886,27 +2206,27 @@ export class EditorView {
|
|
|
1886
2206
|
}
|
|
1887
2207
|
if (bracket.mate === undefined) {
|
|
1888
2208
|
if (this.unmatchedBracket(bracket.at)) {
|
|
1889
|
-
marks.push({ className: 'se-bracket-unmatched', rects: this.
|
|
2209
|
+
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(bracket.at, bracket.at + 1, rows) });
|
|
1890
2210
|
}
|
|
1891
2211
|
continue;
|
|
1892
2212
|
}
|
|
1893
2213
|
for (const offset of [bracket.at, bracket.mate]) {
|
|
1894
2214
|
if (!seen.has(offset)) {
|
|
1895
2215
|
seen.add(offset);
|
|
1896
|
-
marks.push({ className: 'se-bracket-match', rects: this.
|
|
2216
|
+
marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
|
|
1897
2217
|
}
|
|
1898
2218
|
}
|
|
1899
2219
|
}
|
|
1900
2220
|
const selected = selections.flatMap((selection) =>
|
|
1901
2221
|
selection.anchor === selection.head
|
|
1902
2222
|
? []
|
|
1903
|
-
: this.
|
|
2223
|
+
: this.paintRectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows)
|
|
1904
2224
|
);
|
|
1905
2225
|
paintOverlays(this.overlays, { currentLine, selections: selected, focused, marks });
|
|
1906
2226
|
paintOver(this.over, [
|
|
1907
2227
|
{ className: 'se-faded', rects: faded },
|
|
1908
2228
|
{ className: 'se-struck', rects: struck },
|
|
1909
|
-
{ className: 'se-link', rects: this.link === null ? [] : this.
|
|
2229
|
+
{ className: 'se-link', rects: this.link === null ? [] : this.paintRectangles(this.link.from, this.link.to, rows) }
|
|
1910
2230
|
]);
|
|
1911
2231
|
const top = this.scrollTop;
|
|
1912
2232
|
const caretRects = focused
|
|
@@ -1929,38 +2249,84 @@ export class EditorView {
|
|
|
1929
2249
|
this.root.dataset.carets = String(selections.length);
|
|
1930
2250
|
}
|
|
1931
2251
|
|
|
1932
|
-
/*
|
|
1933
|
-
|
|
2252
|
+
/*
|
|
2253
|
+
* The lines the host marked in a layer, which stay on their lines through edits until the layer is set again. Layers
|
|
2254
|
+
* draw together; where two mark the same line the layer that was set first after being empty sits below the later one.
|
|
2255
|
+
*/
|
|
2256
|
+
setChangeMarks(marks: readonly EditorChangeMark[], layer = ''): void {
|
|
1934
2257
|
const last = this.model.getLineCount();
|
|
1935
2258
|
const clamp = (line: number): number => Math.min(last, Math.max(1, Math.trunc(line) || 1)) - 1;
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2259
|
+
if (marks.length === 0) {
|
|
2260
|
+
this.changeLayers.delete(layer);
|
|
2261
|
+
} else {
|
|
2262
|
+
this.changeLayers.set(
|
|
2263
|
+
layer,
|
|
2264
|
+
marks.map((mark) => {
|
|
2265
|
+
const from = this.model.getLine(clamp(mark.startLine)).start;
|
|
2266
|
+
return {
|
|
2267
|
+
kind: mark.kind,
|
|
2268
|
+
from,
|
|
2269
|
+
to: mark.kind === 'deleted' ? from : this.model.getLine(Math.max(clamp(mark.startLine), clamp(mark.endLine))).end
|
|
2270
|
+
};
|
|
2271
|
+
})
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
1940
2274
|
this.changeVersion++;
|
|
1941
2275
|
this.render();
|
|
1942
2276
|
}
|
|
1943
2277
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2278
|
+
/*
|
|
2279
|
+
* How every marked line differs. The layer that last became non-empty wins a shared line for every kind; inside one
|
|
2280
|
+
* layer a removal never covers a line that was added or changed, as its mark sits on the line after the removed text.
|
|
2281
|
+
*/
|
|
2282
|
+
private resolvedChanges(): { lines: Map<number, EditorChangeKind>; runs: { kind: EditorChangeKind; start: number; end: number }[] } {
|
|
2283
|
+
if (this.changeCache?.version === this.changeVersion) {
|
|
2284
|
+
return this.changeCache;
|
|
2285
|
+
}
|
|
2286
|
+
const lines = new Map<number, EditorChangeKind>();
|
|
2287
|
+
for (const marks of this.changeLayers.values()) {
|
|
2288
|
+
const layer = new Map<number, EditorChangeKind>();
|
|
2289
|
+
for (const mark of marks) {
|
|
2290
|
+
const start = this.model.positionAt(mark.from).line;
|
|
2291
|
+
const end = mark.kind === 'deleted' ? start : this.model.positionAt(Math.max(mark.from, mark.to)).line;
|
|
2292
|
+
for (let line = start; line <= end; line++) {
|
|
2293
|
+
if (mark.kind !== 'deleted' || !layer.has(line)) {
|
|
2294
|
+
layer.set(line, mark.kind);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
for (const [line, kind] of layer) {
|
|
2299
|
+
lines.set(line, kind);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const runs: { kind: EditorChangeKind; start: number; end: number }[] = [];
|
|
2303
|
+
for (const line of [...lines.keys()].sort((left, right) => left - right)) {
|
|
2304
|
+
const kind = lines.get(line)!;
|
|
2305
|
+
const run = runs.at(-1);
|
|
2306
|
+
if (run !== undefined && kind !== 'deleted' && run.kind === kind && run.end === line - 1) {
|
|
2307
|
+
run.end = line;
|
|
2308
|
+
} else {
|
|
2309
|
+
runs.push({ kind, start: line, end: line });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
this.changeCache = { version: this.changeVersion, lines, runs };
|
|
2313
|
+
return this.changeCache;
|
|
1947
2314
|
}
|
|
1948
2315
|
|
|
1949
|
-
/* How each line on screen differs
|
|
2316
|
+
/* How each line on screen differs. */
|
|
1950
2317
|
private changedLines(rows: readonly LayoutRow[]): Map<number, EditorChangeKind> {
|
|
1951
|
-
const lines =
|
|
2318
|
+
const { lines } = this.resolvedChanges();
|
|
1952
2319
|
const first = rows[0]?.line ?? 0;
|
|
1953
2320
|
const lastRow = rows.at(-1);
|
|
1954
2321
|
const last = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
}
|
|
2322
|
+
const visible = new Map<number, EditorChangeKind>();
|
|
2323
|
+
for (let line = first; line <= last; line++) {
|
|
2324
|
+
const kind = lines.get(line);
|
|
2325
|
+
if (kind !== undefined) {
|
|
2326
|
+
visible.set(line, kind);
|
|
1961
2327
|
}
|
|
1962
2328
|
}
|
|
1963
|
-
return
|
|
2329
|
+
return visible;
|
|
1964
2330
|
}
|
|
1965
2331
|
|
|
1966
2332
|
/* The carets of agents, each at an offset that follows its text through edits until the host sets them again. */
|
|
@@ -2054,7 +2420,7 @@ export class EditorView {
|
|
|
2054
2420
|
}
|
|
2055
2421
|
|
|
2056
2422
|
/* Lines the host tinted, which follow their text through edits until it sets them again. */
|
|
2057
|
-
setLineHighlights(highlights: readonly
|
|
2423
|
+
setLineHighlights(highlights: readonly EditorLineHighlight[]): void {
|
|
2058
2424
|
this.highlights.set(
|
|
2059
2425
|
highlights.map((highlight) => ({ id: '', ...highlight })),
|
|
2060
2426
|
this.model.getLineCount(),
|
|
@@ -2080,6 +2446,7 @@ export class EditorView {
|
|
|
2080
2446
|
): { className: string; rects: LayoutRect[]; background: string }[] {
|
|
2081
2447
|
const byBackground = new Map<string, LayoutRect[]>();
|
|
2082
2448
|
const width = Math.max(this.layout.width, this.viewportWidth);
|
|
2449
|
+
const emphasisByColor = new Map<string, LayoutRect[]>();
|
|
2083
2450
|
for (const row of rows) {
|
|
2084
2451
|
const highlight = row.kind === 'text' ? lines.get(row.line) : undefined;
|
|
2085
2452
|
if (highlight !== undefined) {
|
|
@@ -2087,14 +2454,32 @@ export class EditorView {
|
|
|
2087
2454
|
const rects = byBackground.get(background) ?? [];
|
|
2088
2455
|
rects.push({ x: 0, y: row.top, width, height: row.height });
|
|
2089
2456
|
byBackground.set(background, rects);
|
|
2457
|
+
if (row.kind === 'text' && highlight.emphasis !== undefined) {
|
|
2458
|
+
const bounds = this.model.getLine(row.line);
|
|
2459
|
+
const emphasisBackground = `color-mix(in srgb, ${colorValue(highlight.color)} 22%, transparent)`;
|
|
2460
|
+
const emphasized = emphasisByColor.get(emphasisBackground) ?? [];
|
|
2461
|
+
for (const range of highlight.emphasis) {
|
|
2462
|
+
emphasized.push(...this.paintRectangles(Math.max(bounds.start, range.from), Math.min(bounds.end, range.to), [row]));
|
|
2463
|
+
}
|
|
2464
|
+
emphasisByColor.set(emphasisBackground, emphasized);
|
|
2465
|
+
}
|
|
2090
2466
|
}
|
|
2091
2467
|
}
|
|
2092
|
-
return [
|
|
2468
|
+
return [
|
|
2469
|
+
...[...byBackground].map(([background, rects]) => ({ className: 'se-line-highlight', rects, background })),
|
|
2470
|
+
...[...emphasisByColor].map(([background, rects]) => ({ className: 'se-line-emphasis', rects, background }))
|
|
2471
|
+
];
|
|
2093
2472
|
}
|
|
2094
2473
|
|
|
2095
2474
|
/* Code that is not in the document, drawn in the editor's own face and colors. */
|
|
2096
2475
|
renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions): void {
|
|
2097
|
-
renderCodeBlock(
|
|
2476
|
+
renderCodeBlock(
|
|
2477
|
+
container,
|
|
2478
|
+
text,
|
|
2479
|
+
options.tokens === undefined ? this.tokens.tokenizeText(text.split(/\r?\n/)) : options.tokens,
|
|
2480
|
+
this.settings.tabSize,
|
|
2481
|
+
options
|
|
2482
|
+
);
|
|
2098
2483
|
}
|
|
2099
2484
|
|
|
2100
2485
|
private attributedLines(first: number, last: number): Map<number, AttributedLines> {
|
|
@@ -2213,9 +2598,8 @@ export class EditorView {
|
|
|
2213
2598
|
const row = this.layout.rowForLine(line);
|
|
2214
2599
|
return row.top + row.height;
|
|
2215
2600
|
};
|
|
2216
|
-
for (const
|
|
2217
|
-
|
|
2218
|
-
spans.push({ kind: mark.kind, top: rowTop(start), bottom: mark.kind === 'deleted' ? rowTop(start) : rowBottom(end) });
|
|
2601
|
+
for (const { kind, start, end } of this.resolvedChanges().runs) {
|
|
2602
|
+
spans.push({ kind, top: rowTop(start), bottom: kind === 'deleted' ? rowTop(start) : rowBottom(end) });
|
|
2219
2603
|
}
|
|
2220
2604
|
for (const marker of this.markers) {
|
|
2221
2605
|
if (marker.severity !== 'hint' && marker.to > marker.from) {
|
|
@@ -2272,6 +2656,9 @@ export class EditorView {
|
|
|
2272
2656
|
}
|
|
2273
2657
|
|
|
2274
2658
|
private stickyAt(scrollTop: number): StickyPlacement[] {
|
|
2659
|
+
if (this.settings.stickyScroll === false) {
|
|
2660
|
+
return [];
|
|
2661
|
+
}
|
|
2275
2662
|
const { layout } = this;
|
|
2276
2663
|
const bottomOf = (line: number): number => {
|
|
2277
2664
|
const row = layout.rowForLine(line);
|
|
@@ -2297,13 +2684,18 @@ export class EditorView {
|
|
|
2297
2684
|
const placements = this.scrollTop <= 0 ? [] : this.stickyAt(this.scrollTop);
|
|
2298
2685
|
const entries = placements.map((placement): StickyEntry => {
|
|
2299
2686
|
const row = this.layout.rowForLine(placement.block.startLine) as TextRow;
|
|
2300
|
-
return {
|
|
2687
|
+
return {
|
|
2688
|
+
line: placement.block.startLine,
|
|
2689
|
+
label: this.lineNumberLabel(placement.block.startLine),
|
|
2690
|
+
geometry: this.layout.geometry(row),
|
|
2691
|
+
tokens: this.styledTokensOf(placement.block.startLine)
|
|
2692
|
+
};
|
|
2301
2693
|
});
|
|
2302
2694
|
const changed =
|
|
2303
2695
|
entries.length !== this.stickyEntries.length ||
|
|
2304
2696
|
entries.some((entry, index) => {
|
|
2305
2697
|
const before = this.stickyEntries[index]!;
|
|
2306
|
-
return entry.line !== before.line || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
|
|
2698
|
+
return entry.line !== before.line || entry.label !== before.label || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
|
|
2307
2699
|
});
|
|
2308
2700
|
this.sticky.hidden = entries.length === 0;
|
|
2309
2701
|
this.sticky.style.setProperty('--se-scroll-left', `${this.scrollLeft}px`);
|
|
@@ -2343,11 +2735,18 @@ export class EditorView {
|
|
|
2343
2735
|
return subscribe(this.gutterActionListeners, listener);
|
|
2344
2736
|
}
|
|
2345
2737
|
|
|
2346
|
-
setGutterMarkers(owner: string, markers: readonly
|
|
2738
|
+
setGutterMarkers(owner: string, markers: readonly GutterMarker[]): void {
|
|
2347
2739
|
const before = this.gutterMarkers.get(owner) ?? [];
|
|
2348
2740
|
if (
|
|
2349
2741
|
before.length === markers.length &&
|
|
2350
|
-
before.every(
|
|
2742
|
+
before.every(
|
|
2743
|
+
(marker, index) =>
|
|
2744
|
+
marker.id === markers[index]!.id &&
|
|
2745
|
+
marker.line === markers[index]!.line &&
|
|
2746
|
+
marker.label === markers[index]!.label &&
|
|
2747
|
+
marker.icon === markers[index]!.icon &&
|
|
2748
|
+
marker.pressed === markers[index]!.pressed
|
|
2749
|
+
)
|
|
2351
2750
|
) {
|
|
2352
2751
|
return;
|
|
2353
2752
|
}
|
|
@@ -2374,12 +2773,12 @@ export class EditorView {
|
|
|
2374
2773
|
}
|
|
2375
2774
|
|
|
2376
2775
|
/* One marker per line: where two owners mark the same line, the owner that sorts first stays. */
|
|
2377
|
-
private markersByLine(): ReadonlyMap<number,
|
|
2378
|
-
const byLine = new Map<number,
|
|
2776
|
+
private markersByLine(): ReadonlyMap<number, GutterMarker> {
|
|
2777
|
+
const byLine = new Map<number, GutterMarker>();
|
|
2379
2778
|
for (const owner of [...this.gutterMarkers.keys()].sort()) {
|
|
2380
2779
|
for (const marker of this.gutterMarkers.get(owner)!) {
|
|
2381
2780
|
if (!byLine.has(marker.line)) {
|
|
2382
|
-
byLine.set(marker.line,
|
|
2781
|
+
byLine.set(marker.line, marker);
|
|
2383
2782
|
}
|
|
2384
2783
|
}
|
|
2385
2784
|
}
|
|
@@ -2408,12 +2807,14 @@ export class EditorView {
|
|
|
2408
2807
|
}
|
|
2409
2808
|
this.disposed = true;
|
|
2410
2809
|
this.cancelScroll();
|
|
2810
|
+
this.find.dispose();
|
|
2411
2811
|
this.subscription.dispose();
|
|
2412
2812
|
this.resizeObserver?.disconnect();
|
|
2413
2813
|
this.blockObserver?.disconnect();
|
|
2414
2814
|
clearTimeout(this.foldTimer);
|
|
2415
2815
|
clearTimeout(this.colorTimer);
|
|
2416
2816
|
clearTimeout(this.noticeTimer);
|
|
2817
|
+
clearTimeout(this.liveTimer);
|
|
2417
2818
|
clearTimeout(this.caretTimer);
|
|
2418
2819
|
if (this.frame !== undefined) {
|
|
2419
2820
|
this.document.defaultView?.cancelAnimationFrame?.(this.frame);
|
|
@@ -2421,7 +2822,9 @@ export class EditorView {
|
|
|
2421
2822
|
this.findListeners.clear();
|
|
2422
2823
|
this.scopeListeners.clear();
|
|
2423
2824
|
this.viewListeners.clear();
|
|
2825
|
+
this.lineHeightListeners.clear();
|
|
2424
2826
|
this.hoverListeners.clear();
|
|
2827
|
+
this.inlayHoverListeners.clear();
|
|
2425
2828
|
this.gutterActionListeners.clear();
|
|
2426
2829
|
this.gutterMarkerListeners.clear();
|
|
2427
2830
|
this.gutterMarkers.clear();
|