@adecore/editor-core 0.23.1 → 0.24.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/bracket-tracker.d.ts +25 -0
- package/dist/bracket-tracker.js +272 -0
- package/dist/brackets.d.ts +35 -0
- package/dist/brackets.js +167 -99
- package/dist/document.d.ts +3 -0
- package/dist/document.js +84 -32
- package/dist/index.d.ts +1 -1
- package/dist/lexical.d.ts +1 -0
- package/dist/lexical.js +13 -3
- package/dist/paste.js +2 -1
- package/dist/rope.js +35 -8
- package/dist/structure.js +4 -1
- package/package.json +1 -1
- package/src/bracket-tracker.ts +321 -0
- package/src/brackets.ts +201 -98
- package/src/document.ts +82 -31
- package/src/index.ts +1 -1
- package/src/lexical.ts +14 -3
- package/src/paste.ts +2 -1
- package/src/rope.ts +43 -12
- package/src/structure.ts +4 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type BracketMatch } from './brackets.ts';
|
|
2
|
+
import type { TextRope } from './rope.ts';
|
|
3
|
+
import type { DocumentChange } from './types.ts';
|
|
4
|
+
export type BracketText = Pick<TextRope, 'length' | 'lineCount' | 'charAt' | 'lineAt' | 'lineBounds' | 'slice'>;
|
|
5
|
+
export declare class BracketTracker {
|
|
6
|
+
private readonly text;
|
|
7
|
+
private readonly stretchLength;
|
|
8
|
+
private language;
|
|
9
|
+
private rules;
|
|
10
|
+
private checkpoints;
|
|
11
|
+
private proven;
|
|
12
|
+
private budget;
|
|
13
|
+
constructor(text: () => BracketText, stretchLength?: number);
|
|
14
|
+
edited(before: BracketText, after: BracketText, changes: readonly DocumentChange[]): void;
|
|
15
|
+
match(offset: number, language: string, limit?: number): BracketMatch | null | undefined;
|
|
16
|
+
private use;
|
|
17
|
+
private nextStale;
|
|
18
|
+
private prove;
|
|
19
|
+
private relex;
|
|
20
|
+
private stretchOf;
|
|
21
|
+
private lexStretch;
|
|
22
|
+
private eventAt;
|
|
23
|
+
private closerAfter;
|
|
24
|
+
private openerBefore;
|
|
25
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { bracketRules, initialBracketState, lexBrackets, sameBracketState } from './brackets.js';
|
|
2
|
+
/* How much text one stretch between checkpoints holds, which is what an edit makes the tracker lex again. */
|
|
3
|
+
const STRETCH_LENGTH = 4096;
|
|
4
|
+
const IGNORE = {
|
|
5
|
+
open() { },
|
|
6
|
+
close() { },
|
|
7
|
+
mismatch() { }
|
|
8
|
+
};
|
|
9
|
+
/*
|
|
10
|
+
* Pairs the bracket at an offset without lexing the document for it. The lexer's state is kept at checkpoints a
|
|
11
|
+
* few kilobytes apart, so an edit lexes only the stretch it is in, and the checkpoints after it count again as
|
|
12
|
+
* soon as the state at one of them comes out the same. Each checkpoint also knows how few brackets were open
|
|
13
|
+
* until the next, which lets a search for a far mate step over whole stretches.
|
|
14
|
+
*/
|
|
15
|
+
export class BracketTracker {
|
|
16
|
+
text;
|
|
17
|
+
stretchLength;
|
|
18
|
+
language = '';
|
|
19
|
+
rules = bracketRules('');
|
|
20
|
+
checkpoints = [];
|
|
21
|
+
/* The first stale checkpoint: what comes before it is proven, and so is its state. */
|
|
22
|
+
proven = 0;
|
|
23
|
+
/* How much more the current question may lex. */
|
|
24
|
+
budget = Number.POSITIVE_INFINITY;
|
|
25
|
+
constructor(text, stretchLength = STRETCH_LENGTH) {
|
|
26
|
+
this.text = text;
|
|
27
|
+
this.stretchLength = stretchLength;
|
|
28
|
+
}
|
|
29
|
+
/* Follows simultaneous changes that turned `before` into `after`, in the coordinates of `before`. */
|
|
30
|
+
edited(before, after, changes) {
|
|
31
|
+
if (this.checkpoints.length === 0 || changes.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const edits = [];
|
|
35
|
+
let delta = 0;
|
|
36
|
+
for (const change of changes) {
|
|
37
|
+
// The line above too, since an edit at the start of a line can turn the `\r` that ends it into a `\r\n`.
|
|
38
|
+
const first = Math.max(0, before.lineAt(change.from) - 1);
|
|
39
|
+
const oldLast = before.lineAt(change.to);
|
|
40
|
+
const newLast = after.lineAt(change.from + delta + change.insertedLength);
|
|
41
|
+
delta += change.insertedLength - (change.to - change.from);
|
|
42
|
+
const previous = edits.at(-1);
|
|
43
|
+
if (previous !== undefined && first <= previous.oldLast) {
|
|
44
|
+
previous.oldLast = oldLast;
|
|
45
|
+
previous.newLast = newLast;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
edits.push({ first, oldLast, newLast });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const kept = [];
|
|
52
|
+
let index = 0;
|
|
53
|
+
let shift = 0;
|
|
54
|
+
for (const checkpoint of this.checkpoints) {
|
|
55
|
+
let dropped = false;
|
|
56
|
+
while (index < edits.length && checkpoint.line > edits[index].first) {
|
|
57
|
+
kept.at(-1).stale = true;
|
|
58
|
+
if (checkpoint.line <= edits[index].oldLast) {
|
|
59
|
+
dropped = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
shift = edits[index].newLast - edits[index].oldLast;
|
|
63
|
+
index++;
|
|
64
|
+
}
|
|
65
|
+
if (!dropped) {
|
|
66
|
+
checkpoint.line += shift;
|
|
67
|
+
kept.push(checkpoint);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (index < edits.length) {
|
|
71
|
+
kept.at(-1).stale = true;
|
|
72
|
+
}
|
|
73
|
+
this.checkpoints = kept;
|
|
74
|
+
this.proven = this.nextStale(0);
|
|
75
|
+
}
|
|
76
|
+
/*
|
|
77
|
+
* The mate of the bracket at `offset`. Undefined where no bracket of the code stands, as in a string or a comment,
|
|
78
|
+
* and null when answering takes lexing more than `limit` characters; the work is kept, so asking again goes on.
|
|
79
|
+
*/
|
|
80
|
+
match(offset, language, limit = Number.POSITIVE_INFINITY) {
|
|
81
|
+
const rope = this.text();
|
|
82
|
+
if (!/^[()[\]{}]$/.test(rope.charAt(offset))) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
this.use(language);
|
|
86
|
+
this.budget = limit;
|
|
87
|
+
const line = rope.lineAt(offset);
|
|
88
|
+
if (!this.prove(line)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const index = this.stretchOf(line);
|
|
92
|
+
const event = this.eventAt(index, offset);
|
|
93
|
+
if (event === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (event.kind === 'mismatch' || event.mate === null) {
|
|
97
|
+
return { mate: undefined };
|
|
98
|
+
}
|
|
99
|
+
if (event.mate !== undefined) {
|
|
100
|
+
return { mate: event.mate };
|
|
101
|
+
}
|
|
102
|
+
const mate = event.kind === 'open' ? this.closerAfter(index, event.depth) : this.openerBefore(index, event.depth);
|
|
103
|
+
return mate === null ? null : { mate };
|
|
104
|
+
}
|
|
105
|
+
use(language) {
|
|
106
|
+
if (language === this.language && this.checkpoints.length > 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.language = language;
|
|
110
|
+
this.rules = bracketRules(language);
|
|
111
|
+
this.checkpoints = [{ line: 0, state: initialBracketState(), low: 0, stale: true }];
|
|
112
|
+
this.proven = 0;
|
|
113
|
+
}
|
|
114
|
+
nextStale(from) {
|
|
115
|
+
let index = from;
|
|
116
|
+
while (index < this.checkpoints.length && !this.checkpoints[index].stale) {
|
|
117
|
+
index++;
|
|
118
|
+
}
|
|
119
|
+
return index;
|
|
120
|
+
}
|
|
121
|
+
/* Lexes until the stretch that holds `line` is proven, or the budget is spent. */
|
|
122
|
+
prove(line) {
|
|
123
|
+
while (this.proven < this.checkpoints.length && this.checkpoints[this.proven].line <= line) {
|
|
124
|
+
if (this.budget <= 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
this.relex(this.proven, line);
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
/* Lexes the stale stretch at `index` again, cut into new ones, and stops early once it is past `until`. */
|
|
132
|
+
relex(index, until) {
|
|
133
|
+
const rope = this.text();
|
|
134
|
+
const next = this.checkpoints[index + 1];
|
|
135
|
+
const stop = next?.line ?? rope.lineCount;
|
|
136
|
+
const state = { ...this.checkpoints[index].state };
|
|
137
|
+
const added = [];
|
|
138
|
+
let current = this.checkpoints[index];
|
|
139
|
+
let line = current.line;
|
|
140
|
+
for (;;) {
|
|
141
|
+
const from = rope.lineBounds(line).start;
|
|
142
|
+
const end = Math.min(stop, from + this.stretchLength >= rope.length ? rope.lineCount : Math.max(line + 1, rope.lineAt(from + this.stretchLength)));
|
|
143
|
+
const to = end >= rope.lineCount ? rope.length : rope.lineBounds(end).start;
|
|
144
|
+
let low = state.stack?.depth ?? 0;
|
|
145
|
+
this.budget -= to - from;
|
|
146
|
+
lexBrackets(state, this.rules, rope.slice(from, to), 0, to - from, from, {
|
|
147
|
+
open() { },
|
|
148
|
+
close(_at, depth) {
|
|
149
|
+
low = Math.min(low, depth - 1);
|
|
150
|
+
},
|
|
151
|
+
mismatch() {
|
|
152
|
+
low = 0;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
current.low = low;
|
|
156
|
+
current.stale = false;
|
|
157
|
+
if (end === stop) {
|
|
158
|
+
if (next !== undefined && !sameBracketState(state, next.state)) {
|
|
159
|
+
next.state = state;
|
|
160
|
+
next.stale = true;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
current = { line: end, state: { ...state }, low: 0, stale: true };
|
|
165
|
+
added.push(current);
|
|
166
|
+
line = end;
|
|
167
|
+
if (end > until || this.budget <= 0) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this.checkpoints.splice(index + 1, 0, ...added);
|
|
172
|
+
this.proven = this.nextStale(index + 1);
|
|
173
|
+
}
|
|
174
|
+
stretchOf(line) {
|
|
175
|
+
let low = 0;
|
|
176
|
+
let high = this.checkpoints.length - 1;
|
|
177
|
+
while (low < high) {
|
|
178
|
+
const middle = Math.ceil((low + high) / 2);
|
|
179
|
+
if (this.checkpoints[middle].line <= line) {
|
|
180
|
+
low = middle;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
high = middle - 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return low;
|
|
187
|
+
}
|
|
188
|
+
lexStretch(index, visitor) {
|
|
189
|
+
const rope = this.text();
|
|
190
|
+
const from = rope.lineBounds(this.checkpoints[index].line).start;
|
|
191
|
+
const next = this.checkpoints[index + 1];
|
|
192
|
+
const to = next === undefined ? rope.length : rope.lineBounds(next.line).start;
|
|
193
|
+
lexBrackets({ ...this.checkpoints[index].state }, this.rules, rope.slice(from, to), 0, to - from, from, visitor);
|
|
194
|
+
}
|
|
195
|
+
/* What the lexer made of the bracket at `offset`, with its mate when the stretch it is in holds that. */
|
|
196
|
+
eventAt(index, offset) {
|
|
197
|
+
let found;
|
|
198
|
+
// The openers this stretch opened; the ones from before it are only counted.
|
|
199
|
+
let opened = [];
|
|
200
|
+
this.lexStretch(index, {
|
|
201
|
+
open(at, depth) {
|
|
202
|
+
opened.push(at);
|
|
203
|
+
if (at === offset) {
|
|
204
|
+
found = { kind: 'open', depth };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
close(at, depth) {
|
|
208
|
+
const opener = opened.pop();
|
|
209
|
+
if (at === offset) {
|
|
210
|
+
found = { kind: 'close', depth, ...(opener === undefined ? {} : { mate: opener }) };
|
|
211
|
+
}
|
|
212
|
+
else if (found?.kind === 'open' && found.mate === undefined && depth === found.depth) {
|
|
213
|
+
found.mate = at;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
mismatch(at) {
|
|
217
|
+
opened = [];
|
|
218
|
+
if (at === offset) {
|
|
219
|
+
found = { kind: 'mismatch', depth: 0 };
|
|
220
|
+
}
|
|
221
|
+
else if (found?.kind === 'open' && found.mate === undefined) {
|
|
222
|
+
found.mate = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return found;
|
|
227
|
+
}
|
|
228
|
+
/* The closer of an opener at `depth` that the stretch at `index` leaves open, undefined when nothing closes it and null when the budget ran out. */
|
|
229
|
+
closerAfter(index, depth) {
|
|
230
|
+
for (let current = index + 1; current < this.checkpoints.length; current++) {
|
|
231
|
+
if (!this.prove(this.checkpoints[current].line)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (this.checkpoints[current].low >= depth) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let result;
|
|
238
|
+
this.lexStretch(current, {
|
|
239
|
+
open() { },
|
|
240
|
+
close(at, closed) {
|
|
241
|
+
if (result === undefined && closed === depth) {
|
|
242
|
+
result = at;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
mismatch() {
|
|
246
|
+
result ??= false;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return result === false ? undefined : result;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
/* The opener at `depth` that is still open where the stretch at `index` starts. */
|
|
254
|
+
openerBefore(index, depth) {
|
|
255
|
+
for (let current = index - 1; current >= 0; current--) {
|
|
256
|
+
if (this.checkpoints[current].low >= depth) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
let result;
|
|
260
|
+
this.lexStretch(current, {
|
|
261
|
+
...IGNORE,
|
|
262
|
+
open(at, opened) {
|
|
263
|
+
if (opened === depth) {
|
|
264
|
+
result = at;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/brackets.d.ts
CHANGED
|
@@ -2,4 +2,39 @@ export interface BracketIndex {
|
|
|
2
2
|
pairs: ReadonlyMap<number, number>;
|
|
3
3
|
unmatched: ReadonlySet<number>;
|
|
4
4
|
}
|
|
5
|
+
export interface BracketMatch {
|
|
6
|
+
mate: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface OpenBracket {
|
|
9
|
+
close: string;
|
|
10
|
+
template: boolean;
|
|
11
|
+
depth: number;
|
|
12
|
+
previous: OpenBracket | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface BracketState {
|
|
15
|
+
mode: 'code' | 'line-comment' | 'block-comment' | 'markup-comment' | 'string' | 'template' | 'regex';
|
|
16
|
+
quote: string;
|
|
17
|
+
inClass: boolean;
|
|
18
|
+
expression: boolean;
|
|
19
|
+
stack: OpenBracket | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface BracketRules {
|
|
22
|
+
script: boolean;
|
|
23
|
+
hash: boolean;
|
|
24
|
+
hashAtWordStart: boolean;
|
|
25
|
+
dash: boolean;
|
|
26
|
+
phpAttributes: boolean;
|
|
27
|
+
slash: boolean;
|
|
28
|
+
block: boolean;
|
|
29
|
+
markup: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface BracketVisitor {
|
|
32
|
+
open(at: number, depth: number): void;
|
|
33
|
+
close(at: number, depth: number): void;
|
|
34
|
+
mismatch(at: number): void;
|
|
35
|
+
}
|
|
36
|
+
export declare function initialBracketState(): BracketState;
|
|
37
|
+
export declare function bracketRules(language: string): BracketRules;
|
|
38
|
+
export declare function sameBracketState(left: BracketState, right: BracketState): boolean;
|
|
39
|
+
export declare function lexBrackets(state: BracketState, rules: BracketRules, text: string, from: number, to: number, offset: number, visitor: BracketVisitor): void;
|
|
5
40
|
export declare function scanBrackets(text: string, language?: string): BracketIndex;
|
package/dist/brackets.js
CHANGED
|
@@ -1,136 +1,204 @@
|
|
|
1
|
-
import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers,
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
return end < 0 ? text.length : end;
|
|
1
|
+
import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpressionAfter, startsExpressionAt, startsWord } from './lexical.js';
|
|
2
|
+
export function initialBracketState() {
|
|
3
|
+
return { mode: 'code', quote: '', inClass: false, expression: true, stack: undefined };
|
|
5
4
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export function bracketRules(language) {
|
|
6
|
+
return {
|
|
7
|
+
script: isScript(language),
|
|
8
|
+
hash: hasHashComments(language),
|
|
9
|
+
hashAtWordStart: hashNeedsWordStart(language),
|
|
10
|
+
dash: hasDashComments(language),
|
|
11
|
+
phpAttributes: isPhp(language),
|
|
12
|
+
slash: hasSlashComments(language),
|
|
13
|
+
block: hasBlockComments(language),
|
|
14
|
+
markup: hasMarkupComments(language)
|
|
15
|
+
};
|
|
9
16
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (text[cursor] === '\\') {
|
|
14
|
-
cursor++;
|
|
15
|
-
}
|
|
16
|
-
else if (text[cursor] === quote || (endsAtLineBreak && /[\r\n]/.test(text[cursor]))) {
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
17
|
+
export function sameBracketState(left, right) {
|
|
18
|
+
if (left.mode !== right.mode || left.quote !== right.quote || left.inClass !== right.inClass || left.expression !== right.expression) {
|
|
19
|
+
return false;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
let one = left.stack;
|
|
22
|
+
let other = right.stack;
|
|
23
|
+
while (one !== other) {
|
|
24
|
+
if (one === undefined || other === undefined || one.close !== other.close || one.template !== other.template || one.depth !== other.depth) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
one = one.previous;
|
|
28
|
+
other = other.previous;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
21
31
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
inClass = false;
|
|
35
|
-
}
|
|
36
|
-
else if ((character === '/' && !inClass) || /[\r\n]/.test(character)) {
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
32
|
+
function isSpace(character) {
|
|
33
|
+
const code = character.charCodeAt(0);
|
|
34
|
+
return code === 32 || (code >= 9 && code <= 13) || (code > 127 && /\s/.test(character));
|
|
35
|
+
}
|
|
36
|
+
function isCloser(character) {
|
|
37
|
+
return character === ')' || character === ']' || character === '}';
|
|
38
|
+
}
|
|
39
|
+
/* Where a comment that runs to `terminator` ends in `text`, or `to` when it goes on past it. */
|
|
40
|
+
function commentEnd(state, text, at, to, terminator) {
|
|
41
|
+
const end = text.indexOf(terminator, at);
|
|
42
|
+
if (end < 0 || end + terminator.length > to) {
|
|
43
|
+
return to;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
state.mode = 'code';
|
|
46
|
+
return end + terminator.length - 1;
|
|
41
47
|
}
|
|
42
|
-
/*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const hashComments = hasHashComments(language);
|
|
49
|
-
const hashAtWordStart = hashNeedsWordStart(language);
|
|
50
|
-
const dashComments = hasDashComments(language);
|
|
51
|
-
const phpAttributes = isPhp(language);
|
|
52
|
-
const slashComments = hasSlashComments(language);
|
|
53
|
-
const blockComments = hasBlockComments(language);
|
|
54
|
-
const markupComments = hasMarkupComments(language);
|
|
55
|
-
let inTemplate = false;
|
|
56
|
-
let expectsExpression = true;
|
|
57
|
-
for (let at = 0; at < text.length; at++) {
|
|
48
|
+
/*
|
|
49
|
+
* Lexes `text` from `from` to `to`, continuing `state`, and reports every bracket of the code at its offset plus
|
|
50
|
+
* `offset`. A piece has to end at a line break or the end of the text, as nothing here looks past one.
|
|
51
|
+
*/
|
|
52
|
+
export function lexBrackets(state, rules, text, from, to, offset, visitor) {
|
|
53
|
+
for (let at = from; at < to; at++) {
|
|
58
54
|
const char = text[at];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
if (state.mode !== 'code') {
|
|
56
|
+
switch (state.mode) {
|
|
57
|
+
case 'line-comment': {
|
|
58
|
+
const end = text.indexOf('\n', at);
|
|
59
|
+
if (end < 0 || end >= to) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
state.mode = 'code';
|
|
63
|
+
at = end;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
case 'block-comment':
|
|
67
|
+
at = commentEnd(state, text, at, to, '*/');
|
|
68
|
+
continue;
|
|
69
|
+
case 'markup-comment':
|
|
70
|
+
at = commentEnd(state, text, at, to, '-->');
|
|
71
|
+
continue;
|
|
72
|
+
case 'string':
|
|
73
|
+
if (char === '\\') {
|
|
74
|
+
at++;
|
|
75
|
+
}
|
|
76
|
+
else if (char === state.quote || (rules.script && (char === '\n' || char === '\r'))) {
|
|
77
|
+
state.mode = 'code';
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
case 'template':
|
|
81
|
+
if (char === '\\') {
|
|
82
|
+
at++;
|
|
83
|
+
}
|
|
84
|
+
else if (char === '`') {
|
|
85
|
+
state.mode = 'code';
|
|
86
|
+
state.expression = false;
|
|
87
|
+
}
|
|
88
|
+
else if (char === '$' && text[at + 1] === '{') {
|
|
89
|
+
at++;
|
|
90
|
+
const depth = (state.stack?.depth ?? 0) + 1;
|
|
91
|
+
state.stack = { close: '}', template: true, depth, previous: state.stack };
|
|
92
|
+
state.mode = 'code';
|
|
93
|
+
state.expression = true;
|
|
94
|
+
visitor.open(offset + at, depth);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
case 'regex':
|
|
98
|
+
if (char === '\\') {
|
|
99
|
+
at++;
|
|
100
|
+
}
|
|
101
|
+
else if (char === '[') {
|
|
102
|
+
state.inClass = true;
|
|
103
|
+
}
|
|
104
|
+
else if (char === ']') {
|
|
105
|
+
state.inClass = false;
|
|
106
|
+
}
|
|
107
|
+
else if ((char === '/' && !state.inClass) || char === '\n' || char === '\r') {
|
|
108
|
+
state.mode = 'code';
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
72
111
|
}
|
|
73
112
|
continue;
|
|
74
113
|
}
|
|
75
|
-
if (
|
|
114
|
+
if (isSpace(char)) {
|
|
76
115
|
continue;
|
|
77
116
|
}
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
const next = text[at + 1];
|
|
118
|
+
if (rules.markup && char === '<' && text.startsWith('<!--', at)) {
|
|
119
|
+
state.mode = 'markup-comment';
|
|
120
|
+
at += 3;
|
|
80
121
|
}
|
|
81
|
-
else if ((
|
|
82
|
-
(
|
|
83
|
-
(
|
|
84
|
-
|
|
122
|
+
else if ((rules.slash && char === '/' && next === '/') ||
|
|
123
|
+
(rules.dash && char === '-' && next === '-') ||
|
|
124
|
+
(rules.hash && char === '#' && !(rules.phpAttributes && next === '[') && (!rules.hashAtWordStart || startsWord(text, at)))) {
|
|
125
|
+
state.mode = 'line-comment';
|
|
85
126
|
}
|
|
86
|
-
else if (
|
|
87
|
-
|
|
127
|
+
else if (rules.block && char === '/' && next === '*') {
|
|
128
|
+
state.mode = 'block-comment';
|
|
129
|
+
at++;
|
|
88
130
|
}
|
|
89
131
|
else if (char === '"' || char === "'") {
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
state.mode = 'string';
|
|
133
|
+
state.quote = char;
|
|
134
|
+
state.expression = false;
|
|
92
135
|
}
|
|
93
|
-
else if (script && char === '`') {
|
|
94
|
-
|
|
136
|
+
else if (rules.script && char === '`') {
|
|
137
|
+
state.mode = 'template';
|
|
95
138
|
}
|
|
96
|
-
else if (script && char === '/' &&
|
|
97
|
-
|
|
98
|
-
|
|
139
|
+
else if (rules.script && char === '/' && state.expression) {
|
|
140
|
+
state.mode = 'regex';
|
|
141
|
+
state.inClass = false;
|
|
142
|
+
state.expression = false;
|
|
99
143
|
}
|
|
100
144
|
else if (openers[char]) {
|
|
101
|
-
stack
|
|
102
|
-
|
|
145
|
+
const depth = (state.stack?.depth ?? 0) + 1;
|
|
146
|
+
state.stack = { close: openers[char], template: false, depth, previous: state.stack };
|
|
147
|
+
state.expression = true;
|
|
148
|
+
visitor.open(offset + at, depth);
|
|
103
149
|
}
|
|
104
|
-
else if (
|
|
105
|
-
const open = stack
|
|
150
|
+
else if (isCloser(char)) {
|
|
151
|
+
const open = state.stack;
|
|
106
152
|
if (open?.close === char) {
|
|
107
|
-
stack.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
state.stack = open.previous;
|
|
154
|
+
if (open.template) {
|
|
155
|
+
state.mode = 'template';
|
|
156
|
+
}
|
|
157
|
+
visitor.close(offset + at, open.depth);
|
|
111
158
|
}
|
|
112
159
|
else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const abandoned of stack.splice(0)) {
|
|
116
|
-
unmatched.add(abandoned.at);
|
|
117
|
-
}
|
|
160
|
+
state.stack = undefined;
|
|
161
|
+
visitor.mismatch(offset + at);
|
|
118
162
|
}
|
|
119
|
-
|
|
163
|
+
state.expression = false;
|
|
120
164
|
}
|
|
121
165
|
else if (isWordCharacter(char)) {
|
|
122
166
|
const start = at;
|
|
123
|
-
while (at + 1 <
|
|
167
|
+
while (at + 1 < to && isWordCharacter(text[at + 1])) {
|
|
124
168
|
at++;
|
|
125
169
|
}
|
|
126
|
-
|
|
170
|
+
state.expression = startsExpressionAt(text, start, at + 1);
|
|
127
171
|
}
|
|
128
172
|
else {
|
|
129
|
-
|
|
173
|
+
state.expression = startsExpressionAfter(char);
|
|
130
174
|
}
|
|
131
175
|
}
|
|
132
|
-
|
|
133
|
-
|
|
176
|
+
}
|
|
177
|
+
/* Lexical pairing for incomplete code; malformed nesting never produces crossing pairs. */
|
|
178
|
+
export function scanBrackets(text, language = 'typescript') {
|
|
179
|
+
const pairs = new Map();
|
|
180
|
+
const unmatched = new Set();
|
|
181
|
+
let open = [];
|
|
182
|
+
lexBrackets(initialBracketState(), bracketRules(language), text, 0, text.length, 0, {
|
|
183
|
+
open(at) {
|
|
184
|
+
open.push(at);
|
|
185
|
+
},
|
|
186
|
+
close(at) {
|
|
187
|
+
const opener = open.pop();
|
|
188
|
+
pairs.set(opener, at);
|
|
189
|
+
pairs.set(at, opener);
|
|
190
|
+
},
|
|
191
|
+
mismatch(at) {
|
|
192
|
+
unmatched.add(at);
|
|
193
|
+
// A mismatched closer keeps a distant opener from claiming it later.
|
|
194
|
+
for (const abandoned of open) {
|
|
195
|
+
unmatched.add(abandoned);
|
|
196
|
+
}
|
|
197
|
+
open = [];
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
for (const at of open) {
|
|
201
|
+
unmatched.add(at);
|
|
134
202
|
}
|
|
135
203
|
return { pairs, unmatched };
|
|
136
204
|
}
|
package/dist/document.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BracketMatch } from './brackets.ts';
|
|
1
2
|
import type { DocumentLine } from './rope.ts';
|
|
2
3
|
import type { FindMatch, FindNextOptions, FindOptions } from './search.ts';
|
|
3
4
|
import type { FoldingOptions, FoldingRange } from './structure.ts';
|
|
@@ -47,6 +48,7 @@ export declare class DocumentModel {
|
|
|
47
48
|
occurrencesExhausted: boolean;
|
|
48
49
|
private closers;
|
|
49
50
|
private bracketCache?;
|
|
51
|
+
private bracketTracker;
|
|
50
52
|
private scriptCache?;
|
|
51
53
|
constructor(text?: string);
|
|
52
54
|
getText(): string;
|
|
@@ -55,6 +57,7 @@ export declare class DocumentModel {
|
|
|
55
57
|
getLineCount(): number;
|
|
56
58
|
getLine(line: number): DocumentLine;
|
|
57
59
|
slice(from: number, to: number): string;
|
|
60
|
+
matchBracket(offset: number, language?: string, limit?: number): BracketMatch | null | undefined;
|
|
58
61
|
getSelections(): readonly Selection[];
|
|
59
62
|
getSelectionCount(): number;
|
|
60
63
|
getPrimary(): Selection;
|