@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Affinity, LineGeometry } from './layout.ts';
|
|
2
|
+
|
|
3
|
+
export function stopCount(geometry: LineGeometry): number {
|
|
4
|
+
return geometry.uniform === undefined ? geometry.offsets.length : geometry.text.length + 1;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function offsetOfStop(geometry: LineGeometry, stop: number): number {
|
|
8
|
+
return geometry.uniform === undefined ? geometry.offsets[stop]! : stop;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function rowOfStop(geometry: LineGeometry, stop: number): number {
|
|
12
|
+
if (geometry.uniform === undefined) {
|
|
13
|
+
return geometry.rowOf[stop]!;
|
|
14
|
+
}
|
|
15
|
+
let first = 0;
|
|
16
|
+
let last = geometry.rowStarts.length;
|
|
17
|
+
while (first < last) {
|
|
18
|
+
const middle = (first + last) >>> 1;
|
|
19
|
+
if (geometry.rowStarts[middle]! <= stop) {
|
|
20
|
+
first = middle + 1;
|
|
21
|
+
} else {
|
|
22
|
+
last = middle;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return Math.max(0, first - 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function xOfStop(geometry: LineGeometry, stop: number, affinity: Affinity = 'before'): number {
|
|
29
|
+
if (geometry.uniform === undefined) {
|
|
30
|
+
return geometry[affinity][stop]!;
|
|
31
|
+
}
|
|
32
|
+
const row = rowOfStop(geometry, stop);
|
|
33
|
+
return geometry.uniform[row === 0 ? 'first' : 'continuation'][stop - geometry.rowStarts[row]!]!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function stopAtOffset(geometry: LineGeometry, offset: number): number {
|
|
37
|
+
if (geometry.uniform !== undefined) {
|
|
38
|
+
return Math.min(geometry.text.length, Math.max(0, Math.ceil(offset)));
|
|
39
|
+
}
|
|
40
|
+
let first = 0;
|
|
41
|
+
let last = geometry.offsets.length;
|
|
42
|
+
while (first < last) {
|
|
43
|
+
const middle = (first + last) >>> 1;
|
|
44
|
+
if (geometry.offsets[middle]! < offset) {
|
|
45
|
+
first = middle + 1;
|
|
46
|
+
} else {
|
|
47
|
+
last = middle;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Math.min(geometry.offsets.length - 1, first);
|
|
51
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
|
|
2
|
+
import type { LineTokenizer } from './types.ts';
|
|
3
|
+
|
|
4
|
+
type Schedule = (callback: () => void, delay: number) => () => void;
|
|
5
|
+
const scheduleTimer: Schedule = (callback, delay) => {
|
|
6
|
+
const timer = setTimeout(callback, delay);
|
|
7
|
+
return () => clearTimeout(timer);
|
|
8
|
+
};
|
|
9
|
+
const MAX_BUFFER = 64 * 1024;
|
|
10
|
+
|
|
11
|
+
/* Edits collect language declarations without making every key wait for grammar loading. */
|
|
12
|
+
export class GrammarPreparation {
|
|
13
|
+
private tokenizer: LineTokenizer | null = null;
|
|
14
|
+
private generation = 0;
|
|
15
|
+
private pending: string[] = [];
|
|
16
|
+
private size = 0;
|
|
17
|
+
private lastLine = -1;
|
|
18
|
+
private full = false;
|
|
19
|
+
private running = false;
|
|
20
|
+
private due = false;
|
|
21
|
+
private paused = false;
|
|
22
|
+
private disposed = false;
|
|
23
|
+
private cancelTimer: (() => void) | null = null;
|
|
24
|
+
|
|
25
|
+
private readonly model: Pick<DocumentModel, 'getText' | 'getLine' | 'positionAt'>;
|
|
26
|
+
private readonly changed: () => void;
|
|
27
|
+
private readonly schedule: Schedule;
|
|
28
|
+
|
|
29
|
+
constructor(model: Pick<DocumentModel, 'getText' | 'getLine' | 'positionAt'>, changed: () => void, schedule: Schedule = scheduleTimer) {
|
|
30
|
+
this.model = model;
|
|
31
|
+
this.changed = changed;
|
|
32
|
+
this.schedule = schedule;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
begin(): void {
|
|
36
|
+
this.generation++;
|
|
37
|
+
this.tokenizer = null;
|
|
38
|
+
this.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ready(tokenizer: LineTokenizer | null): void {
|
|
42
|
+
this.tokenizer = tokenizer;
|
|
43
|
+
this.scheduleFlush();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
edited(batches: readonly (readonly DocumentChange[])[]): void {
|
|
47
|
+
if (this.disposed || this.paused) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (this.tokenizer === null || batches.length !== 1) {
|
|
51
|
+
this.full = true;
|
|
52
|
+
this.lastLine = -1;
|
|
53
|
+
} else if (this.tokenizer.prepare === undefined) {
|
|
54
|
+
return;
|
|
55
|
+
} else if (!this.full) {
|
|
56
|
+
let shift = 0;
|
|
57
|
+
let through = -1;
|
|
58
|
+
for (const change of batches[0]!) {
|
|
59
|
+
const first = this.model.positionAt(change.from + shift).line;
|
|
60
|
+
const last = this.model.positionAt(change.from + shift + change.insertedLength).line;
|
|
61
|
+
shift += change.insertedLength - (change.to - change.from);
|
|
62
|
+
for (let line = Math.max(first, through + 1); line <= last; line++) {
|
|
63
|
+
const text = this.model.getLine(line).text;
|
|
64
|
+
const replacing = batches[0]!.length === 1 && first === last && line === this.lastLine;
|
|
65
|
+
if (replacing) {
|
|
66
|
+
this.size -= this.pending.pop()!.length + 1;
|
|
67
|
+
}
|
|
68
|
+
this.size += text.length + 1;
|
|
69
|
+
if (this.size > MAX_BUFFER) {
|
|
70
|
+
this.full = true;
|
|
71
|
+
this.pending = [];
|
|
72
|
+
this.lastLine = -1;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
this.pending.push(text);
|
|
76
|
+
this.lastLine = line;
|
|
77
|
+
}
|
|
78
|
+
through = last;
|
|
79
|
+
if (this.full) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.scheduleFlush();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pause(paused: boolean): void {
|
|
88
|
+
if (this.paused === paused) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.paused = paused;
|
|
92
|
+
this.generation++;
|
|
93
|
+
this.clear();
|
|
94
|
+
if (!paused) {
|
|
95
|
+
this.full = true;
|
|
96
|
+
this.scheduleFlush();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
dispose(): void {
|
|
101
|
+
this.disposed = true;
|
|
102
|
+
this.generation++;
|
|
103
|
+
this.clear();
|
|
104
|
+
this.tokenizer = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private clear(): void {
|
|
108
|
+
this.cancelTimer?.();
|
|
109
|
+
this.cancelTimer = null;
|
|
110
|
+
this.pending = [];
|
|
111
|
+
this.size = 0;
|
|
112
|
+
this.lastLine = -1;
|
|
113
|
+
this.full = false;
|
|
114
|
+
this.due = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private scheduleFlush(): void {
|
|
118
|
+
if (this.disposed || this.paused || this.tokenizer?.prepare === undefined || (!this.full && this.pending.length === 0)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.cancelTimer?.();
|
|
122
|
+
this.due = false;
|
|
123
|
+
this.cancelTimer = this.schedule(() => {
|
|
124
|
+
this.cancelTimer = null;
|
|
125
|
+
this.due = true;
|
|
126
|
+
void this.flush();
|
|
127
|
+
}, 200);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async flush(): Promise<void> {
|
|
131
|
+
const tokenizer = this.tokenizer;
|
|
132
|
+
if (this.running || this.disposed || this.paused || tokenizer?.prepare === undefined || !this.due) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const generation = this.generation;
|
|
136
|
+
const text = this.full ? this.model.getText() : this.pending.join('\n');
|
|
137
|
+
this.pending = [];
|
|
138
|
+
this.size = 0;
|
|
139
|
+
this.lastLine = -1;
|
|
140
|
+
this.full = false;
|
|
141
|
+
this.due = false;
|
|
142
|
+
this.running = true;
|
|
143
|
+
try {
|
|
144
|
+
await tokenizer.prepare(text);
|
|
145
|
+
if (!this.disposed && !this.paused && generation === this.generation && tokenizer === this.tokenizer) {
|
|
146
|
+
// A load completed while paused may already have rebuilt this grammar.
|
|
147
|
+
this.changed();
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Keep the current colors; a later edit may retry the failed load.
|
|
151
|
+
} finally {
|
|
152
|
+
this.running = false;
|
|
153
|
+
if (this.due) {
|
|
154
|
+
void this.flush();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
|
|
1
2
|
export { AGENT_COLORS } from './attribution.ts';
|
|
3
|
+
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
|
|
2
4
|
export { createSmartEditorEngine } from './engine.ts';
|
|
3
5
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
4
6
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
@@ -28,18 +30,24 @@ export type {
|
|
|
28
30
|
EditorFindState,
|
|
29
31
|
EditorReplaceOptions,
|
|
30
32
|
EditorGutterAction,
|
|
33
|
+
EditorGutterIcon,
|
|
31
34
|
EditorGutterMarker,
|
|
35
|
+
EditorReplacement,
|
|
32
36
|
EditorIndentation,
|
|
33
37
|
EditorContentChange,
|
|
34
38
|
EditorHighlight,
|
|
35
39
|
EditorHighlightKind,
|
|
36
40
|
EditorHover,
|
|
37
41
|
EditorGhostText,
|
|
42
|
+
EditorInlayClick,
|
|
43
|
+
EditorInlayClickHandler,
|
|
38
44
|
EditorInlayHint,
|
|
45
|
+
EditorInlayTarget,
|
|
39
46
|
EditorKeyHandler,
|
|
40
47
|
EditorLineAction,
|
|
41
48
|
EditorLineHighlight,
|
|
42
49
|
EditorMarker,
|
|
50
|
+
EditorInputPopup,
|
|
43
51
|
EditorMessages,
|
|
44
52
|
EditorMarkerSeverity,
|
|
45
53
|
EditorReveal,
|
|
@@ -49,6 +57,7 @@ export type {
|
|
|
49
57
|
EditorPosition,
|
|
50
58
|
EditorRemoteCursor,
|
|
51
59
|
EditorRange,
|
|
60
|
+
EditorApplyEditsOptions,
|
|
52
61
|
EditorTextChange,
|
|
53
62
|
EditorTrackedRange,
|
|
54
63
|
EditorRunCommand,
|
|
@@ -59,6 +68,7 @@ export type {
|
|
|
59
68
|
FoldRole,
|
|
60
69
|
KeyChord,
|
|
61
70
|
LineToken,
|
|
71
|
+
LineTokenKind,
|
|
62
72
|
ScopeColorSource,
|
|
63
73
|
ScopeColors,
|
|
64
74
|
ScopeStyle,
|
package/src/keymap-table.ts
CHANGED
|
@@ -105,6 +105,8 @@ export const KEYMAP = {
|
|
|
105
105
|
goToImplementation: bind('Mod+Alt+B'),
|
|
106
106
|
peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
|
|
107
107
|
peekReferences: bind('Alt+F7'),
|
|
108
|
+
callHierarchy: bind('Ctrl+Alt+H'),
|
|
109
|
+
toggleRenderedDocs: bind('Ctrl+Alt+Q'),
|
|
108
110
|
historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
|
|
109
111
|
historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
|
|
110
112
|
recentLocations: bind('Mod+Shift+E'),
|
|
@@ -120,7 +122,10 @@ export const KEYMAP = {
|
|
|
120
122
|
|
|
121
123
|
export type KeymapId = keyof typeof KEYMAP;
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
/* Commands added after a host may have written a full keymap of its own; a keymap without one falls back to the built-in chord. */
|
|
126
|
+
export type OptionalKeymapId = 'toggleRenderedDocs';
|
|
127
|
+
|
|
128
|
+
export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
|
|
124
129
|
export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
|
|
125
130
|
|
|
126
131
|
export const KEYMAP_IDS = Object.keys(KEYMAP) as KeymapId[];
|
|
@@ -131,7 +136,7 @@ export function resolveKeymap(overrides: KeymapOverrides = {}): Keymap {
|
|
|
131
136
|
|
|
132
137
|
/* The chord of a command on a platform, or null when the platform has none. */
|
|
133
138
|
export function chordOf(id: KeymapId, apple: boolean, keymap: Keymap = KEYMAP): string | null {
|
|
134
|
-
return keymap[id][apple ? 'mac' : 'other'];
|
|
139
|
+
return (keymap[id] ?? KEYMAP[id])[apple ? 'mac' : 'other'];
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
const MODIFIERS = new Map<string, 'mod' | 'ctrl' | 'meta' | 'alt' | 'shift'>([
|
|
@@ -161,3 +166,40 @@ export function parseChord(text: string): KeyChord {
|
|
|
161
166
|
}
|
|
162
167
|
return chord;
|
|
163
168
|
}
|
|
169
|
+
|
|
170
|
+
const BASE_PUNCTUATION: Readonly<Record<string, string>> = {
|
|
171
|
+
Comma: ',',
|
|
172
|
+
Period: '.',
|
|
173
|
+
Slash: '/',
|
|
174
|
+
Semicolon: ';',
|
|
175
|
+
Quote: "'",
|
|
176
|
+
Backquote: '`',
|
|
177
|
+
Backslash: '\\',
|
|
178
|
+
BracketLeft: '[',
|
|
179
|
+
BracketRight: ']',
|
|
180
|
+
Minus: '-',
|
|
181
|
+
Equal: '='
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/* The character a physical key has with no layer on it, null for a key that has none, such as an arrow or Enter. */
|
|
185
|
+
function baseCharacter(code: string): string | null {
|
|
186
|
+
const letter = /^Key([A-Z])$/.exec(code);
|
|
187
|
+
if (letter !== null) {
|
|
188
|
+
return letter[1].toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
const digit = /^(?:Digit|Numpad)([0-9])$/.exec(code);
|
|
191
|
+
return digit !== null ? digit[1] : (BASE_PUNCTUATION[code] ?? null);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* Whether Ctrl and Alt together type a character instead of pressing a chord. AltGr reports both, on Windows even
|
|
196
|
+
* for the left Ctrl and Alt, and a browser may report AltGraph for Option, so the state is no evidence: the character is.
|
|
197
|
+
* Never on Apple platforms, where Option types with no Ctrl.
|
|
198
|
+
*/
|
|
199
|
+
export function typesInsteadOfChord(event: { key: string; code?: string; ctrlKey: boolean; altKey: boolean }, apple: boolean): boolean {
|
|
200
|
+
if (apple || !event.ctrlKey || !event.altKey || event.code === undefined || [...event.key].length !== 1) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const base = baseCharacter(event.code);
|
|
204
|
+
return base !== null && event.key.toLowerCase() !== base;
|
|
205
|
+
}
|
package/src/keymap.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditorCommand } from '@adecore/editor-core';
|
|
2
|
-
import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord } from './keymap-table.ts';
|
|
2
|
+
import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord, typesInsteadOfChord } from './keymap-table.ts';
|
|
3
3
|
import type { EditorViewCommand, KeyChord } from './types.ts';
|
|
4
4
|
|
|
5
5
|
export interface KeyLike {
|
|
@@ -10,6 +10,7 @@ export interface KeyLike {
|
|
|
10
10
|
metaKey: boolean;
|
|
11
11
|
altKey: boolean;
|
|
12
12
|
shiftKey: boolean;
|
|
13
|
+
getModifierState?(key: string): boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export type MoveKey = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'PageUp' | 'PageDown';
|
|
@@ -129,6 +130,9 @@ function keyMatches(chordKey: string, event: KeyLike): boolean {
|
|
|
129
130
|
|
|
130
131
|
/* Whether an event is a chord, with `mod` as Cmd on macOS and Ctrl elsewhere and every modifier the chord does not name up. */
|
|
131
132
|
export function chordMatches(chord: KeyChord, event: KeyLike, apple: boolean): boolean {
|
|
133
|
+
if (typesInsteadOfChord(event, apple)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
132
136
|
const wantsMeta = chord.meta || (chord.mod && apple);
|
|
133
137
|
const wantsCtrl = chord.ctrl || (chord.mod && !apple);
|
|
134
138
|
return (
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DocumentChange } from '@adecore/editor-core';
|
|
2
|
+
import { offsetMapper } from './offsets.ts';
|
|
3
|
+
|
|
4
|
+
export interface LineBounds {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function lineChangeMapper(batches: readonly (readonly DocumentChange[])[]): (bounds: LineBounds) => LineBounds | null {
|
|
10
|
+
const steps = batches.map((changes) => ({ changes, map: offsetMapper(changes) }));
|
|
11
|
+
return (bounds) => {
|
|
12
|
+
let { start, end } = bounds;
|
|
13
|
+
for (const { changes, map } of steps) {
|
|
14
|
+
let first = 0;
|
|
15
|
+
let last = changes.length;
|
|
16
|
+
while (first < last) {
|
|
17
|
+
const middle = (first + last) >>> 1;
|
|
18
|
+
if (changes[middle]!.to < start) {
|
|
19
|
+
first = middle + 1;
|
|
20
|
+
} else {
|
|
21
|
+
last = middle;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Touching either boundary can merge lines or change a CRLF ending.
|
|
25
|
+
if (changes[first] !== undefined && changes[first]!.from <= end) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
start = map(start);
|
|
29
|
+
end = map(end);
|
|
30
|
+
}
|
|
31
|
+
return { start, end };
|
|
32
|
+
};
|
|
33
|
+
}
|