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