@adecore/editor 0.38.0 → 0.40.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/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/lens.ts
CHANGED
|
@@ -12,17 +12,20 @@ export function lensSignature(entries: readonly EditorCodeVisionEntry[]): string
|
|
|
12
12
|
export function fillLens(container: HTMLElement, entries: readonly EditorCodeVisionEntry[], lookup: (id: string) => EditorCodeVisionEntry | undefined): void {
|
|
13
13
|
const document = container.ownerDocument;
|
|
14
14
|
container.classList.add('se-lens');
|
|
15
|
+
|
|
15
16
|
for (const entry of entries) {
|
|
16
17
|
const button = document.createElement('button');
|
|
17
18
|
button.type = 'button';
|
|
18
19
|
button.className = 'se-lens-entry';
|
|
19
20
|
button.tabIndex = -1;
|
|
21
|
+
|
|
20
22
|
if (entry.icon !== undefined) {
|
|
21
23
|
const icon = document.createElement('span');
|
|
22
24
|
icon.className = `se-lens-icon se-lens-icon-${entry.icon}`;
|
|
23
25
|
icon.setAttribute('aria-hidden', 'true');
|
|
24
26
|
button.append(icon);
|
|
25
27
|
}
|
|
28
|
+
|
|
26
29
|
button.append(entry.text);
|
|
27
30
|
// The editor keeps the focus, so the caret stays where it was and a popover can hand the keyboard back.
|
|
28
31
|
button.addEventListener('pointerdown', (event) => event.preventDefault());
|
package/src/listeners.ts
CHANGED
package/src/metrics.ts
CHANGED
|
@@ -11,53 +11,67 @@ const FALLBACK: EditorFont = { family: 'monospace', size: 13, lineHeight: 20 };
|
|
|
11
11
|
|
|
12
12
|
function pixels(value: string): number | null {
|
|
13
13
|
const parsed = Number.parseFloat(value);
|
|
14
|
+
|
|
14
15
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/* The code face the page gives the editor's element, in whole pixels so a line is as tall as the viewer's. */
|
|
18
19
|
export function readEditorFont(element: HTMLElement): EditorFont {
|
|
19
20
|
const view = element.ownerDocument.defaultView;
|
|
21
|
+
|
|
20
22
|
if (!view?.getComputedStyle) {
|
|
21
23
|
return FALLBACK;
|
|
22
24
|
}
|
|
25
|
+
|
|
23
26
|
const style = view.getComputedStyle(element);
|
|
24
27
|
const size = pixels(style.fontSize) ?? FALLBACK.size;
|
|
25
28
|
const lineHeight = pixels(style.lineHeight) ?? Math.round(size * 1.5);
|
|
29
|
+
|
|
26
30
|
return { family: style.fontFamily || FALLBACK.family, size, lineHeight: Math.round(lineHeight) };
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/* Character widths from a canvas, which sets the text in the same face the page draws it in. Without a canvas a character is 0.6 of the size. */
|
|
30
34
|
export function createMetrics(font: EditorFont, tabSize: number, document: Document): LayoutMetrics {
|
|
31
35
|
let context: CanvasRenderingContext2D | null = null;
|
|
36
|
+
|
|
32
37
|
try {
|
|
33
38
|
context = document.createElement('canvas').getContext('2d');
|
|
34
39
|
} catch {
|
|
35
40
|
context = null;
|
|
36
41
|
}
|
|
42
|
+
|
|
37
43
|
const codeFont = `${font.size}px ${font.family}`;
|
|
38
44
|
const inlayFont = `${Math.max(12, font.size - 1)}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;
|
|
39
45
|
const cache = new Map<string, number>();
|
|
46
|
+
|
|
40
47
|
const measure = (text: string, face: string, fallback: number): number => {
|
|
41
48
|
if (!context) {
|
|
42
49
|
return Array.from(text).length * fallback;
|
|
43
50
|
}
|
|
51
|
+
|
|
44
52
|
context.font = face;
|
|
45
53
|
context.fontKerning = 'none';
|
|
54
|
+
|
|
46
55
|
return context.measureText(text).width;
|
|
47
56
|
};
|
|
57
|
+
|
|
48
58
|
return {
|
|
49
59
|
lineHeight: font.lineHeight,
|
|
50
60
|
charWidth: measure('M', codeFont, font.size * 0.6),
|
|
51
61
|
tabSize,
|
|
52
62
|
measureText(text) {
|
|
53
63
|
let width = cache.get(text);
|
|
64
|
+
|
|
54
65
|
if (width === undefined) {
|
|
55
66
|
width = measure(text, codeFont, font.size * 0.6);
|
|
67
|
+
|
|
56
68
|
if (cache.size > 2000) {
|
|
57
69
|
cache.clear();
|
|
58
70
|
}
|
|
71
|
+
|
|
59
72
|
cache.set(text, width);
|
|
60
73
|
}
|
|
74
|
+
|
|
61
75
|
return width;
|
|
62
76
|
},
|
|
63
77
|
measureInlay: (text) => measure(text, inlayFont, font.size * 0.5)
|
package/src/modifier-gesture.ts
CHANGED
|
@@ -46,6 +46,7 @@ export class DoubleModifierGesture {
|
|
|
46
46
|
/* Another modifier held with the one that counts spoils the gesture. */
|
|
47
47
|
private spoiled(event: GestureEvent): boolean {
|
|
48
48
|
const other = this.modifier === 'Alt' ? event.ctrlKey : event.altKey;
|
|
49
|
+
|
|
49
50
|
return other || event.metaKey || event.shiftKey;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -54,24 +55,34 @@ export class DoubleModifierGesture {
|
|
|
54
55
|
if (event.repeat === true) {
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
58
|
+
|
|
57
59
|
if (this.spoiled(event)) {
|
|
58
60
|
this.reset();
|
|
61
|
+
|
|
59
62
|
return null;
|
|
60
63
|
}
|
|
64
|
+
|
|
61
65
|
if (this.firstPressed && event.timeStamp - this.last > FORGET_MS) {
|
|
62
66
|
this.reset();
|
|
63
67
|
}
|
|
68
|
+
|
|
64
69
|
this.step(event, 'press');
|
|
70
|
+
|
|
65
71
|
return null;
|
|
66
72
|
}
|
|
73
|
+
|
|
67
74
|
if (this.armed) {
|
|
68
75
|
const held = this.modifier === 'Alt' ? event.altKey : event.ctrlKey;
|
|
76
|
+
|
|
69
77
|
if (held && !this.spoiled(event) && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
70
78
|
return event.key === 'ArrowUp' ? 'above' : 'below';
|
|
71
79
|
}
|
|
80
|
+
|
|
72
81
|
return null;
|
|
73
82
|
}
|
|
83
|
+
|
|
74
84
|
this.reset();
|
|
85
|
+
|
|
75
86
|
return null;
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -79,8 +90,10 @@ export class DoubleModifierGesture {
|
|
|
79
90
|
if (event.key === this.modifier) {
|
|
80
91
|
if (this.spoiled(event)) {
|
|
81
92
|
this.reset();
|
|
93
|
+
|
|
82
94
|
return;
|
|
83
95
|
}
|
|
96
|
+
|
|
84
97
|
this.step(event, 'release');
|
|
85
98
|
} else if (!this.armed) {
|
|
86
99
|
this.reset();
|
|
@@ -90,8 +103,10 @@ export class DoubleModifierGesture {
|
|
|
90
103
|
private step(event: GestureEvent, kind: 'press' | 'release'): void {
|
|
91
104
|
if (this.firstPressed && event.timeStamp - this.last > TAP_MS) {
|
|
92
105
|
this.reset();
|
|
106
|
+
|
|
93
107
|
return;
|
|
94
108
|
}
|
|
109
|
+
|
|
95
110
|
if (kind === 'press') {
|
|
96
111
|
if (!this.firstPressed) {
|
|
97
112
|
this.reset();
|
|
@@ -103,8 +118,10 @@ export class DoubleModifierGesture {
|
|
|
103
118
|
} else {
|
|
104
119
|
this.reset();
|
|
105
120
|
}
|
|
121
|
+
|
|
106
122
|
return;
|
|
107
123
|
}
|
|
124
|
+
|
|
108
125
|
if (this.firstPressed && !this.firstReleased) {
|
|
109
126
|
this.firstReleased = true;
|
|
110
127
|
this.last = event.timeStamp;
|
package/src/native-input.ts
CHANGED
|
@@ -49,6 +49,7 @@ export class NativeInput {
|
|
|
49
49
|
if (this.composing) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
|
|
52
53
|
const selection = this.model.getPrimary();
|
|
53
54
|
const caretCount = this.model.getSelectionCount();
|
|
54
55
|
const length = this.model.getLength();
|
|
@@ -56,33 +57,41 @@ export class NativeInput {
|
|
|
56
57
|
const end = Math.max(selection.anchor, selection.head);
|
|
57
58
|
let from = 0;
|
|
58
59
|
let to = length;
|
|
60
|
+
|
|
59
61
|
if (length > CONTEXT) {
|
|
60
62
|
from = Math.max(0, Math.min(start, selection.head - 2048));
|
|
61
63
|
to = Math.min(length, end + 2048);
|
|
64
|
+
|
|
62
65
|
if (to - from > CONTEXT) {
|
|
63
66
|
from = Math.max(0, selection.head - CONTEXT / 2);
|
|
64
67
|
to = Math.min(length, from + CONTEXT);
|
|
65
68
|
}
|
|
69
|
+
|
|
66
70
|
// A boundary must not split a surrogate pair or a CRLF.
|
|
67
71
|
if (from > 0 && /[\uDC00-\uDFFF\n]/.test(this.model.slice(from, from + 1))) {
|
|
68
72
|
from--;
|
|
69
73
|
}
|
|
74
|
+
|
|
70
75
|
if (to < length && /[\uDC00-\uDFFF\n]/.test(this.model.slice(to, to + 1))) {
|
|
71
76
|
to++;
|
|
72
77
|
}
|
|
73
78
|
}
|
|
79
|
+
|
|
74
80
|
const source = this.model.slice(from, to);
|
|
75
81
|
const value = textareaText(source);
|
|
76
82
|
const nativeStart = textareaOffset(source, Math.max(0, start - from));
|
|
77
83
|
const nativeEnd = textareaOffset(source, Math.max(0, end - from));
|
|
78
84
|
this.context = { from, source, value, start: nativeStart, end: nativeEnd };
|
|
79
85
|
this.writing = true;
|
|
86
|
+
|
|
80
87
|
if (this.element.value !== value) {
|
|
81
88
|
this.element.value = value;
|
|
82
89
|
}
|
|
90
|
+
|
|
83
91
|
this.element.setSelectionRange(nativeStart, nativeEnd, selection.head < selection.anchor ? 'backward' : 'forward');
|
|
84
92
|
this.writing = false;
|
|
85
93
|
const position = this.model.positionAt(selection.head);
|
|
94
|
+
|
|
86
95
|
this.element.setAttribute(
|
|
87
96
|
'aria-description',
|
|
88
97
|
`Line ${position.line + 1}, column ${position.column + 1}. ${caretCount} caret${caretCount === 1 ? '' : 's'}.`
|
|
@@ -94,11 +103,14 @@ export class NativeInput {
|
|
|
94
103
|
if (this.writing || this.composing || this.element.value !== this.context.value) {
|
|
95
104
|
return;
|
|
96
105
|
}
|
|
106
|
+
|
|
97
107
|
const start = this.element.selectionStart;
|
|
98
108
|
const end = this.element.selectionEnd;
|
|
109
|
+
|
|
99
110
|
if (start === this.context.start && end === this.context.end) {
|
|
100
111
|
return;
|
|
101
112
|
}
|
|
113
|
+
|
|
102
114
|
const from = this.context.from + modelOffset(this.context.source, start);
|
|
103
115
|
const to = this.context.from + modelOffset(this.context.source, end);
|
|
104
116
|
this.host.selectionChanged(this.element.selectionDirection === 'backward' ? { anchor: to, head: from } : { anchor: from, head: to });
|
|
@@ -107,26 +119,35 @@ export class NativeInput {
|
|
|
107
119
|
onInput(): void {
|
|
108
120
|
if (this.composing) {
|
|
109
121
|
this.host.preview(this.composedText());
|
|
122
|
+
|
|
110
123
|
return;
|
|
111
124
|
}
|
|
125
|
+
|
|
112
126
|
if (this.host.readOnly()) {
|
|
113
127
|
this.sync();
|
|
128
|
+
|
|
114
129
|
return;
|
|
115
130
|
}
|
|
131
|
+
|
|
116
132
|
const context = this.context;
|
|
117
133
|
const change = findChange(context.value, this.element.value);
|
|
134
|
+
|
|
118
135
|
if (change.from === change.to && !change.text) {
|
|
119
136
|
return;
|
|
120
137
|
}
|
|
138
|
+
|
|
121
139
|
if (this.model.getSelections().length > 1) {
|
|
122
140
|
this.host.replace(this.replacedText(context));
|
|
141
|
+
|
|
123
142
|
return;
|
|
124
143
|
}
|
|
144
|
+
|
|
125
145
|
const edit = {
|
|
126
146
|
from: context.from + modelOffset(context.source, change.from),
|
|
127
147
|
to: context.from + modelOffset(context.source, change.to),
|
|
128
148
|
text: this.normalizeNewlines(change.text)
|
|
129
149
|
};
|
|
150
|
+
|
|
130
151
|
const next = context.source.slice(0, edit.from - context.from) + edit.text + context.source.slice(edit.to - context.from);
|
|
131
152
|
const start = context.from + modelOffset(next, this.element.selectionStart);
|
|
132
153
|
const end = context.from + modelOffset(next, this.element.selectionEnd);
|
|
@@ -137,6 +158,7 @@ export class NativeInput {
|
|
|
137
158
|
if (this.host.readOnly()) {
|
|
138
159
|
return;
|
|
139
160
|
}
|
|
161
|
+
|
|
140
162
|
this.compositionContext = { ...this.context };
|
|
141
163
|
this.composing = true;
|
|
142
164
|
}
|
|
@@ -145,12 +167,14 @@ export class NativeInput {
|
|
|
145
167
|
if (!this.composing) {
|
|
146
168
|
return;
|
|
147
169
|
}
|
|
170
|
+
|
|
148
171
|
const text = this.composedText();
|
|
149
172
|
const context = this.compositionContext!;
|
|
150
173
|
const unchanged = this.element.value === context.value;
|
|
151
174
|
this.composing = false;
|
|
152
175
|
this.compositionContext = undefined;
|
|
153
176
|
this.host.preview(undefined);
|
|
177
|
+
|
|
154
178
|
if (data && !unchanged && !this.host.readOnly()) {
|
|
155
179
|
if (DEAD_KEY_RESULT.test(text)) {
|
|
156
180
|
for (const character of text) {
|
|
@@ -160,6 +184,7 @@ export class NativeInput {
|
|
|
160
184
|
this.host.replace(text);
|
|
161
185
|
}
|
|
162
186
|
}
|
|
187
|
+
|
|
163
188
|
this.sync();
|
|
164
189
|
}
|
|
165
190
|
|
|
@@ -167,6 +192,7 @@ export class NativeInput {
|
|
|
167
192
|
normalizeNewlines(text: string): string {
|
|
168
193
|
const first = this.model.getLine(0);
|
|
169
194
|
const newline = this.model.slice(first.end, first.next) === '\r\n' ? '\r\n' : '\n';
|
|
195
|
+
|
|
170
196
|
return textareaText(text).replace(/\n/g, newline);
|
|
171
197
|
}
|
|
172
198
|
|
|
@@ -177,6 +203,7 @@ export class NativeInput {
|
|
|
177
203
|
/* What the textarea now holds where the selection of `context` was, the text after it kept. */
|
|
178
204
|
private replacedText(context: InputContext): string {
|
|
179
205
|
const suffix = context.value.length - context.end;
|
|
206
|
+
|
|
180
207
|
return this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix));
|
|
181
208
|
}
|
|
182
209
|
}
|
package/src/offsets.ts
CHANGED
|
@@ -3,38 +3,48 @@ import type { DocumentChange } from '@adecore/editor-core';
|
|
|
3
3
|
/* Where an offset is after a batch of changes. One inside a replaced range lands after the new text. */
|
|
4
4
|
export function mapOffset(offset: number, changes: readonly DocumentChange[]): number {
|
|
5
5
|
let delta = 0;
|
|
6
|
+
|
|
6
7
|
for (const change of changes) {
|
|
7
8
|
if (offset < change.from) {
|
|
8
9
|
break;
|
|
9
10
|
}
|
|
11
|
+
|
|
10
12
|
if (offset < change.to || (offset === change.from && change.from === change.to)) {
|
|
11
13
|
return change.from + delta + change.insertedLength;
|
|
12
14
|
}
|
|
15
|
+
|
|
13
16
|
delta += change.insertedLength - (change.to - change.from);
|
|
14
17
|
}
|
|
18
|
+
|
|
15
19
|
return offset + delta;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/* Build once per sorted, nonoverlapping batch when many offsets need to follow it. */
|
|
19
23
|
export function offsetMapper(changes: readonly DocumentChange[]): (offset: number) => number {
|
|
20
24
|
const deltas = new Float64Array(changes.length + 1);
|
|
25
|
+
|
|
21
26
|
for (let index = 0; index < changes.length; index++) {
|
|
22
27
|
const change = changes[index]!;
|
|
23
28
|
deltas[index + 1] = deltas[index]! + change.insertedLength - (change.to - change.from);
|
|
24
29
|
}
|
|
30
|
+
|
|
25
31
|
return (offset) => {
|
|
26
32
|
let low = 0,
|
|
27
33
|
high = changes.length;
|
|
34
|
+
|
|
28
35
|
while (low < high) {
|
|
29
36
|
const middle = (low + high) >>> 1;
|
|
30
37
|
const change = changes[middle]!;
|
|
38
|
+
|
|
31
39
|
if (change.to < offset || (change.to === offset && change.from !== change.to)) {
|
|
32
40
|
low = middle + 1;
|
|
33
41
|
} else {
|
|
34
42
|
high = middle;
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
|
|
37
46
|
const change = changes[low];
|
|
47
|
+
|
|
38
48
|
return change !== undefined && change.from <= offset ? change.from + deltas[low]! + change.insertedLength : offset + deltas[low]!;
|
|
39
49
|
};
|
|
40
50
|
}
|
package/src/outline.ts
CHANGED
|
@@ -27,10 +27,13 @@ export interface OutlineDocument {
|
|
|
27
27
|
|
|
28
28
|
const MODIFIERS =
|
|
29
29
|
'(?:export\\s+|default\\s+|declare\\s+|abstract\\s+|async\\s+|pub(?:\\([^)]*\\))?\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|readonly\\s+|override\\s+|unsafe\\s+|extern\\s+)*';
|
|
30
|
+
|
|
30
31
|
const DECLARATION = new RegExp(
|
|
31
32
|
`^${MODIFIERS}(function\\*?|class|interface|enum|type|namespace|module|trait|impl|struct|fn|def|func|object|record)\\s+([A-Za-z_$][\\w$]*)`
|
|
32
33
|
);
|
|
34
|
+
|
|
33
35
|
const VARIABLE = new RegExp(`^${MODIFIERS}(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)`);
|
|
36
|
+
|
|
34
37
|
const KEYWORDS = new Set([
|
|
35
38
|
'if',
|
|
36
39
|
'for',
|
|
@@ -49,8 +52,10 @@ const KEYWORDS = new Set([
|
|
|
49
52
|
'super',
|
|
50
53
|
'finally'
|
|
51
54
|
]);
|
|
55
|
+
|
|
52
56
|
const METHOD = /^(?:(?:public|private|protected|static|async|get|set|override|readonly|abstract)\s+)*\*?\s*([A-Za-z_$#][\w$]*)\s*[(<]/;
|
|
53
57
|
const PROPERTY = /^["']?([\w$-]+)["']?\s*:\s*[{[(]?$/;
|
|
58
|
+
|
|
54
59
|
const KINDS: Record<string, string> = {
|
|
55
60
|
'function*': 'function',
|
|
56
61
|
fn: 'function',
|
|
@@ -62,30 +67,39 @@ const KINDS: Record<string, string> = {
|
|
|
62
67
|
trait: 'interface',
|
|
63
68
|
impl: 'class'
|
|
64
69
|
};
|
|
70
|
+
|
|
65
71
|
const LABEL_LIMIT = 48;
|
|
66
72
|
|
|
67
73
|
/* What a header line declares, when it declares something a person would look for by name. */
|
|
68
74
|
export function describeHeader(text: string): { name?: string; kind?: string } {
|
|
69
75
|
const line = text.trim();
|
|
70
76
|
const declaration = DECLARATION.exec(line);
|
|
77
|
+
|
|
71
78
|
if (declaration) {
|
|
72
79
|
return { name: declaration[2]!, kind: KINDS[declaration[1]!] ?? declaration[1]! };
|
|
73
80
|
}
|
|
81
|
+
|
|
74
82
|
const variable = VARIABLE.exec(line);
|
|
83
|
+
|
|
75
84
|
if (variable) {
|
|
76
85
|
return { name: variable[1]!, kind: 'variable' };
|
|
77
86
|
}
|
|
87
|
+
|
|
78
88
|
const method = METHOD.exec(line);
|
|
89
|
+
|
|
79
90
|
if (method && !KEYWORDS.has(method[1]!)) {
|
|
80
91
|
return { name: method[1]!, kind: 'method' };
|
|
81
92
|
}
|
|
93
|
+
|
|
82
94
|
const property = PROPERTY.exec(line);
|
|
95
|
+
|
|
83
96
|
return property ? { name: property[1]!, kind: 'property' } : {};
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/* The header line as a label: what a breadcrumb shows for a block that declares nothing. */
|
|
87
100
|
export function headerLabel(text: string): string {
|
|
88
101
|
const label = text.trim().replace(/[\s{(:]+$/, '');
|
|
102
|
+
|
|
89
103
|
return label.length > LABEL_LIMIT ? `${label.slice(0, LABEL_LIMIT - 1)}…` : label;
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -101,27 +115,34 @@ export function structuralEntries(
|
|
|
101
115
|
mate: (offset: number) => number | undefined
|
|
102
116
|
): OutlineEntry[] {
|
|
103
117
|
const entries: OutlineEntry[] = [];
|
|
118
|
+
|
|
104
119
|
for (const range of ranges) {
|
|
105
120
|
if (range.kind === 'comment' || (range.kind === 'bracket' && slice(range.from, range.from + 1) !== '{')) {
|
|
106
121
|
continue;
|
|
107
122
|
}
|
|
123
|
+
|
|
108
124
|
let headerLine = range.startLine;
|
|
109
125
|
const header = document.getLine(headerLine);
|
|
110
126
|
const lead = header.text.length - header.text.trimStart().length;
|
|
127
|
+
|
|
111
128
|
if (/^[)\]]/.test(header.text.slice(lead))) {
|
|
112
129
|
const opener = mate(header.start + lead);
|
|
130
|
+
|
|
113
131
|
if (opener !== undefined && opener < header.start) {
|
|
114
132
|
headerLine = document.positionAt(opener).line;
|
|
115
133
|
}
|
|
116
134
|
}
|
|
135
|
+
|
|
117
136
|
const start = document.getLine(headerLine);
|
|
118
137
|
entries.push({ from: start.start, to: document.getLine(range.endLine).end, ...describeHeader(start.text) });
|
|
119
138
|
}
|
|
139
|
+
|
|
120
140
|
return entries;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
function toBlock(entry: OutlineEntry, document: OutlineDocument): OutlineBlock {
|
|
124
144
|
const startLine = document.positionAt(entry.from).line;
|
|
145
|
+
|
|
125
146
|
return {
|
|
126
147
|
startLine,
|
|
127
148
|
endLine: Math.max(startLine, document.positionAt(entry.to).line),
|
|
@@ -149,15 +170,21 @@ export class Outline {
|
|
|
149
170
|
/* Blocks the host drew from its own source, in one-based lines; null goes back to the structure. */
|
|
150
171
|
setProvided(blocks: readonly EditorBlock[] | null, document: OutlineDocument): void {
|
|
151
172
|
this.version++;
|
|
173
|
+
|
|
152
174
|
if (blocks === null) {
|
|
153
175
|
this.provided = null;
|
|
176
|
+
|
|
154
177
|
return;
|
|
155
178
|
}
|
|
179
|
+
|
|
156
180
|
const last = document.getLineCount() - 1;
|
|
181
|
+
|
|
157
182
|
const clamp = (line: number): number => Math.min(last, Math.max(0, Math.trunc(line) - 1));
|
|
183
|
+
|
|
158
184
|
this.provided = blocks.map((block) => {
|
|
159
185
|
const start = document.getLine(clamp(block.startLine));
|
|
160
186
|
const header = block.name === undefined ? headerLabel(start.text) : block.name;
|
|
187
|
+
|
|
161
188
|
return {
|
|
162
189
|
from: start.start,
|
|
163
190
|
to: document.getLine(Math.max(clamp(block.startLine), clamp(block.endLine))).end,
|
|
@@ -169,16 +196,20 @@ export class Outline {
|
|
|
169
196
|
|
|
170
197
|
edited(batches: readonly (readonly DocumentChange[])[]): void {
|
|
171
198
|
const mappers = batches.map(offsetMapper);
|
|
199
|
+
|
|
172
200
|
const move = (entries: OutlineEntry[]): OutlineEntry[] =>
|
|
173
201
|
entries.map((entry) => {
|
|
174
202
|
let from = entry.from;
|
|
175
203
|
let to = entry.to;
|
|
204
|
+
|
|
176
205
|
for (const map of mappers) {
|
|
177
206
|
from = map(from);
|
|
178
207
|
to = map(to);
|
|
179
208
|
}
|
|
209
|
+
|
|
180
210
|
return { ...entry, from, to };
|
|
181
211
|
});
|
|
212
|
+
|
|
182
213
|
this.structure = move(this.structure);
|
|
183
214
|
this.provided = this.provided === null ? null : move(this.provided);
|
|
184
215
|
this.version++;
|
|
@@ -190,8 +221,10 @@ export class Outline {
|
|
|
190
221
|
const sorted = (this.provided ?? this.structure)
|
|
191
222
|
.map((entry) => toBlock(entry, document))
|
|
192
223
|
.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
224
|
+
|
|
193
225
|
this.cache = { version: this.version, blocks: sorted.filter((block, index) => index === 0 || block.startLine !== sorted[index - 1]!.startLine) };
|
|
194
226
|
}
|
|
227
|
+
|
|
195
228
|
return this.cache.blocks;
|
|
196
229
|
}
|
|
197
230
|
}
|
|
@@ -220,9 +253,11 @@ export interface StickyPlacement {
|
|
|
220
253
|
*/
|
|
221
254
|
export function stickyPlacements(blocks: readonly OutlineBlock[], scrollTop: number, lineHeight: number, max: number, rows: StickyRows): StickyPlacement[] {
|
|
222
255
|
const placements: StickyPlacement[] = [];
|
|
256
|
+
|
|
223
257
|
for (let depth = 0; depth < max; depth++) {
|
|
224
258
|
const pinnedAt = scrollTop + depth * lineHeight;
|
|
225
259
|
const parent = placements.at(-1)?.block;
|
|
260
|
+
|
|
226
261
|
const next = blocks.find(
|
|
227
262
|
(block) =>
|
|
228
263
|
(parent === undefined || (block.startLine > parent.startLine && block.endLine <= parent.endLine)) &&
|
|
@@ -230,11 +265,14 @@ export function stickyPlacements(blocks: readonly OutlineBlock[], scrollTop: num
|
|
|
230
265
|
rows.top(block.startLine) < pinnedAt &&
|
|
231
266
|
rows.bottom(block.endLine) > pinnedAt
|
|
232
267
|
);
|
|
268
|
+
|
|
233
269
|
if (next === undefined) {
|
|
234
270
|
break;
|
|
235
271
|
}
|
|
272
|
+
|
|
236
273
|
placements.push({ block: next, depth, offset: Math.min(0, rows.bottom(next.endLine) - (pinnedAt + lineHeight)) });
|
|
237
274
|
}
|
|
275
|
+
|
|
238
276
|
return placements;
|
|
239
277
|
}
|
|
240
278
|
|
package/src/overview.ts
CHANGED
|
@@ -28,7 +28,9 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
|
|
|
28
28
|
if (trackHeight <= 0) {
|
|
29
29
|
return [];
|
|
30
30
|
}
|
|
31
|
+
|
|
31
32
|
const scale = trackHeight / Math.max(contentHeight, trackHeight);
|
|
33
|
+
|
|
32
34
|
const ticks = spans
|
|
33
35
|
.map((span): OverviewTick => ({
|
|
34
36
|
kind: span.kind,
|
|
@@ -38,18 +40,24 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
|
|
|
38
40
|
...(span.title === undefined ? {} : { title: span.title })
|
|
39
41
|
}))
|
|
40
42
|
.sort((left, right) => left.y - right.y);
|
|
43
|
+
|
|
41
44
|
const merged: OverviewTick[] = [];
|
|
42
45
|
const last = new Map<OverviewKind, OverviewTick>();
|
|
46
|
+
|
|
43
47
|
for (const tick of ticks) {
|
|
44
48
|
const before = last.get(tick.kind);
|
|
49
|
+
|
|
45
50
|
if (before && tick.y <= before.y + before.height) {
|
|
46
51
|
before.height = Math.max(before.height, tick.y + tick.height - before.y);
|
|
52
|
+
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
55
|
+
|
|
49
56
|
const next = { ...tick };
|
|
50
57
|
last.set(tick.kind, next);
|
|
51
58
|
merged.push(next);
|
|
52
59
|
}
|
|
60
|
+
|
|
53
61
|
return merged;
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -58,7 +66,9 @@ const PAINT_ORDER: Partial<Record<OverviewKind, number>> = { warning: 1, error:
|
|
|
58
66
|
|
|
59
67
|
export function paintOverview(container: HTMLElement, ticks: readonly OverviewTick[]): void {
|
|
60
68
|
const document = container.ownerDocument;
|
|
69
|
+
|
|
61
70
|
const order = (tick: OverviewTick): number => PAINT_ORDER[tick.kind] ?? 0;
|
|
71
|
+
|
|
62
72
|
container.replaceChildren(
|
|
63
73
|
...[...ticks]
|
|
64
74
|
.sort((left, right) => order(left) - order(right))
|
|
@@ -67,12 +77,15 @@ export function paintOverview(container: HTMLElement, ticks: readonly OverviewTi
|
|
|
67
77
|
element.className = `se-tick se-tick-${tick.kind}`;
|
|
68
78
|
element.style.top = `${tick.y}px`;
|
|
69
79
|
element.style.height = `${tick.height}px`;
|
|
80
|
+
|
|
70
81
|
if (tick.offset !== undefined) {
|
|
71
82
|
element.setAttribute('data-offset', String(tick.offset));
|
|
72
83
|
}
|
|
84
|
+
|
|
73
85
|
if (tick.title !== undefined) {
|
|
74
86
|
element.setAttribute('data-title', tick.title);
|
|
75
87
|
}
|
|
88
|
+
|
|
76
89
|
return element;
|
|
77
90
|
})
|
|
78
91
|
);
|