@adecore/editor-core 0.23.1 → 0.24.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.
@@ -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
+ }
@@ -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, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
2
- function skipLine(text, at) {
3
- const end = text.indexOf('\n', at + 1);
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 skipComment(text, from, terminator) {
7
- const end = text.indexOf(terminator, from);
8
- return end < 0 ? text.length : end + terminator.length - 1;
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 skipString(text, at, quote, endsAtLineBreak) {
11
- let cursor = at + 1;
12
- for (; cursor < text.length; cursor++) {
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
- return cursor;
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 skipRegex(text, at) {
23
- let inClass = false;
24
- let cursor = at + 1;
25
- for (; cursor < text.length; cursor++) {
26
- const character = text[cursor];
27
- if (character === '\\') {
28
- cursor++;
29
- }
30
- else if (character === '[') {
31
- inClass = true;
32
- }
33
- else if (character === ']') {
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
- return cursor;
45
+ state.mode = 'code';
46
+ return end + terminator.length - 1;
41
47
  }
42
- /* Lexical pairing for incomplete code; malformed nesting never produces crossing pairs. */
43
- export function scanBrackets(text, language = 'typescript') {
44
- const pairs = new Map();
45
- const unmatched = new Set();
46
- const stack = [];
47
- const script = isScript(language);
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
- const next = text[at + 1];
60
- if (inTemplate) {
61
- if (char === '\\') {
62
- at++;
63
- }
64
- else if (char === '`') {
65
- inTemplate = false;
66
- expectsExpression = false;
67
- }
68
- else if (char === '$' && next === '{') {
69
- stack.push({ at: ++at, close: '}', template: true });
70
- inTemplate = false;
71
- expectsExpression = true;
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 (/\s/.test(char)) {
114
+ if (isSpace(char)) {
76
115
  continue;
77
116
  }
78
- if (markupComments && text.startsWith('<!--', at)) {
79
- at = skipComment(text, at + 4, '-->');
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 ((slashComments && char === '/' && next === '/') ||
82
- (dashComments && char === '-' && next === '-') ||
83
- (hashComments && char === '#' && !(phpAttributes && next === '[') && (!hashAtWordStart || startsWord(text, at)))) {
84
- at = skipLine(text, at);
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 (blockComments && char === '/' && next === '*') {
87
- at = skipComment(text, at + 2, '*/');
127
+ else if (rules.block && char === '/' && next === '*') {
128
+ state.mode = 'block-comment';
129
+ at++;
88
130
  }
89
131
  else if (char === '"' || char === "'") {
90
- at = skipString(text, at, char, script);
91
- expectsExpression = false;
132
+ state.mode = 'string';
133
+ state.quote = char;
134
+ state.expression = false;
92
135
  }
93
- else if (script && char === '`') {
94
- inTemplate = true;
136
+ else if (rules.script && char === '`') {
137
+ state.mode = 'template';
95
138
  }
96
- else if (script && char === '/' && expectsExpression) {
97
- at = skipRegex(text, at);
98
- expectsExpression = false;
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.push({ at, close: openers[char] });
102
- expectsExpression = true;
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 (/[)\]}]/.test(char)) {
105
- const open = stack.at(-1);
150
+ else if (isCloser(char)) {
151
+ const open = state.stack;
106
152
  if (open?.close === char) {
107
- stack.pop();
108
- pairs.set(open.at, at);
109
- pairs.set(at, open.at);
110
- inTemplate = open.template === true;
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
- unmatched.add(at);
114
- // A mismatched closer keeps a distant opener from claiming it later.
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
- expectsExpression = false;
163
+ state.expression = false;
120
164
  }
121
165
  else if (isWordCharacter(char)) {
122
166
  const start = at;
123
- while (at + 1 < text.length && isWordCharacter(text[at + 1])) {
167
+ while (at + 1 < to && isWordCharacter(text[at + 1])) {
124
168
  at++;
125
169
  }
126
- expectsExpression = startsExpression(text.slice(start, at + 1));
170
+ state.expression = startsExpressionAt(text, start, at + 1);
127
171
  }
128
172
  else {
129
- expectsExpression = startsExpressionAfter(char);
173
+ state.expression = startsExpressionAfter(char);
130
174
  }
131
175
  }
132
- for (const open of stack) {
133
- unmatched.add(open.at);
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
  }
@@ -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;