@singapore-editor/find 0.1.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/README.md +28 -0
- package/dist/findController.d.ts +175 -0
- package/dist/findController.d.ts.map +1 -0
- package/dist/findController.js +681 -0
- package/dist/findController.js.map +1 -0
- package/dist/findIcons.d.ts +49 -0
- package/dist/findIcons.d.ts.map +1 -0
- package/dist/findIcons.js +65 -0
- package/dist/findIcons.js.map +1 -0
- package/dist/findWidget.d.ts +45 -0
- package/dist/findWidget.d.ts.map +1 -0
- package/dist/findWidget.js +194 -0
- package/dist/findWidget.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +408 -0
- package/dist/plugin.js.map +1 -0
- package/dist/replacePattern.d.ts +18 -0
- package/dist/replacePattern.d.ts.map +1 -0
- package/dist/replacePattern.js +212 -0
- package/dist/replacePattern.js.map +1 -0
- package/dist/search.d.ts +83 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +470 -0
- package/dist/search.js.map +1 -0
- package/dist/style.css +198 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +48 -0
package/dist/search.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { codePointSizeAt, compareTextOffsetRanges, isWholeWordRange } from "@singapore-editor/core/document";
|
|
2
|
+
//#region src/search.ts
|
|
3
|
+
var FIND_MATCHES_LIMIT = 19999;
|
|
4
|
+
var FIND_REPLACE_ALL_LIMIT = 1073741824;
|
|
5
|
+
var FIND_WINDOW_UNITS = 65536;
|
|
6
|
+
var LINE_SAFE_ESCAPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"d",
|
|
8
|
+
"w",
|
|
9
|
+
"t",
|
|
10
|
+
"r",
|
|
11
|
+
"f",
|
|
12
|
+
"v",
|
|
13
|
+
"b",
|
|
14
|
+
"B",
|
|
15
|
+
"0"
|
|
16
|
+
]);
|
|
17
|
+
function findTextSourceFromSnapshot(snapshot) {
|
|
18
|
+
return {
|
|
19
|
+
length: snapshot.length,
|
|
20
|
+
readRange: (start, end) => snapshot.readRange(start, end),
|
|
21
|
+
lineStartsView: {
|
|
22
|
+
length: snapshot.lineCount,
|
|
23
|
+
at: (index) => index < 0 || index >= snapshot.lineCount ? void 0 : snapshot.lineStart(index),
|
|
24
|
+
indexForOffset: (offset) => snapshot.lineAt(offset)
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function findMatches(source, query, ranges = null, captureMatches = false, limit = FIND_MATCHES_LIMIT) {
|
|
29
|
+
const compiled = compileFindQuery(query);
|
|
30
|
+
if (!compiled || limit <= 0) return [];
|
|
31
|
+
const matches = [];
|
|
32
|
+
const collect = (start, end, captures) => {
|
|
33
|
+
matches.push({
|
|
34
|
+
start,
|
|
35
|
+
end,
|
|
36
|
+
matches: captures
|
|
37
|
+
});
|
|
38
|
+
return matches.length < limit;
|
|
39
|
+
};
|
|
40
|
+
for (const range of searchRanges(source, ranges)) {
|
|
41
|
+
scanRange(source, compiled, range, captureMatches, collect);
|
|
42
|
+
if (matches.length >= limit) break;
|
|
43
|
+
}
|
|
44
|
+
return matches;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every match counted, none kept: a total costs no record per match.
|
|
48
|
+
*
|
|
49
|
+
* With `before`, only the matches starting in front of that offset, which is the
|
|
50
|
+
* position of a match the listing stopped short of.
|
|
51
|
+
*/
|
|
52
|
+
function countMatches(source, query, ranges = null, before = Number.POSITIVE_INFINITY) {
|
|
53
|
+
const compiled = compileFindQuery(query);
|
|
54
|
+
if (!compiled) return 0;
|
|
55
|
+
let count = 0;
|
|
56
|
+
const tally = (start) => {
|
|
57
|
+
if (start >= before) return false;
|
|
58
|
+
count += 1;
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
for (const range of searchRanges(source, ranges)) {
|
|
62
|
+
if (range.start >= before) break;
|
|
63
|
+
scanRange(source, compiled, range, false, tally);
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
/** The line holding `offset`, break excluded. */
|
|
68
|
+
function findLineRange(source, offset) {
|
|
69
|
+
const index = source.lineStartsView.indexForOffset(offset);
|
|
70
|
+
return {
|
|
71
|
+
start: source.lineStartsView.at(index) ?? 0,
|
|
72
|
+
end: lineEndAt(source, index)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One match, scanned for from an offset instead of picked out of a list.
|
|
77
|
+
*
|
|
78
|
+
* A listing stops at FIND_MATCHES_LIMIT, and past that every entry in it sits
|
|
79
|
+
* behind the cursor: picking from it answers each press with the top of the
|
|
80
|
+
* document. Scanning outward stops at the first match it reaches, so how far
|
|
81
|
+
* navigation can go is not what a paint budget allowed.
|
|
82
|
+
*/
|
|
83
|
+
function findNextMatchFrom(source, query, offset, ranges = null, options = {}) {
|
|
84
|
+
const compiled = compileFindQuery(query);
|
|
85
|
+
if (!compiled) return null;
|
|
86
|
+
const searched = searchRanges(source, ranges);
|
|
87
|
+
const ahead = firstMatchAfter(source, compiled, searched, clampSourceOffset(source, offset), options);
|
|
88
|
+
if (ahead) return ahead;
|
|
89
|
+
if (options.loop === false) return null;
|
|
90
|
+
return firstMatchAtOrAfter(source, compiled, searched, searched[0]?.start ?? 0, options);
|
|
91
|
+
}
|
|
92
|
+
function findPreviousMatchFrom(source, query, offset, ranges = null, options = {}) {
|
|
93
|
+
const compiled = compileFindQuery(query);
|
|
94
|
+
if (!compiled) return null;
|
|
95
|
+
const searched = searchRanges(source, ranges);
|
|
96
|
+
const behind = lastMatchBefore(source, compiled, searched, clampSourceOffset(source, offset), options);
|
|
97
|
+
if (behind) return behind;
|
|
98
|
+
if (options.loop === false) return null;
|
|
99
|
+
return lastMatchBefore(source, compiled, searched, source.length, {
|
|
100
|
+
...options,
|
|
101
|
+
escapeEmptyMatchAtOffset: false
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function findMatchIndex(matches, range) {
|
|
105
|
+
return matches.findIndex((match) => match.start === range.start && match.end === range.end);
|
|
106
|
+
}
|
|
107
|
+
function escapeRegExpCharacters(value) {
|
|
108
|
+
return value.replace(/[\\{}*+?|^$.[\]()]/g, "\\$&");
|
|
109
|
+
}
|
|
110
|
+
function lineEndAt(source, index) {
|
|
111
|
+
const nextLineStart = source.lineStartsView.at(index + 1);
|
|
112
|
+
return nextLineStart === void 0 ? source.length : nextLineStart - 1;
|
|
113
|
+
}
|
|
114
|
+
function escapesEmptyMatch(match, offset, escape) {
|
|
115
|
+
return escape && match.start === match.end && match.start === offset;
|
|
116
|
+
}
|
|
117
|
+
function firstMatchAfter(source, query, ranges, offset, options) {
|
|
118
|
+
const found = firstMatchAtOrAfter(source, query, ranges, offset, options);
|
|
119
|
+
if (!found) return null;
|
|
120
|
+
if (!escapesEmptyMatch(found, offset, options.escapeEmptyMatchAtOffset ?? false)) return found;
|
|
121
|
+
return firstMatchAtOrAfter(source, query, ranges, nextCodePointOffset(source, offset), options);
|
|
122
|
+
}
|
|
123
|
+
function firstMatchAtOrAfter(source, query, ranges, offset, options) {
|
|
124
|
+
for (const range of ranges) {
|
|
125
|
+
if (range.end < offset) continue;
|
|
126
|
+
const found = firstMatchInRange(source, query, range, offset, options.captureMatches ?? false);
|
|
127
|
+
if (found) return found;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function firstMatchInRange(source, query, range, offset, captureMatches) {
|
|
132
|
+
let found = null;
|
|
133
|
+
scanRange(source, query, {
|
|
134
|
+
start: forwardScanStart(source, query, range, offset),
|
|
135
|
+
end: range.end
|
|
136
|
+
}, captureMatches, (start, end, captures) => {
|
|
137
|
+
if (start < offset) return true;
|
|
138
|
+
found = {
|
|
139
|
+
start,
|
|
140
|
+
end,
|
|
141
|
+
matches: captures
|
|
142
|
+
};
|
|
143
|
+
return false;
|
|
144
|
+
});
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
function forwardScanStart(source, query, range, offset) {
|
|
148
|
+
if (!isLineLocalPlan(query.plan)) return range.start;
|
|
149
|
+
const lineStartsView = source.lineStartsView;
|
|
150
|
+
const line = lineStartsView.indexForOffset(Math.max(range.start, offset));
|
|
151
|
+
return Math.max(range.start, lineStartsView.at(line) ?? 0);
|
|
152
|
+
}
|
|
153
|
+
function isLineLocalPlan(plan) {
|
|
154
|
+
return plan.kind === "lines" || plan.kind === "anchored-lines";
|
|
155
|
+
}
|
|
156
|
+
function lastMatchBefore(source, query, ranges, offset, options) {
|
|
157
|
+
for (const range of ranges.toReversed()) {
|
|
158
|
+
if (range.start > offset) continue;
|
|
159
|
+
const found = lastMatchInRange(source, query, range, offset, options);
|
|
160
|
+
if (found) return found;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function lastMatchInRange(source, query, searched, offset, options) {
|
|
165
|
+
if (!isLineLocalPlan(query.plan)) return lastMatchInWindow(source, query, searched, offset, options);
|
|
166
|
+
const range = {
|
|
167
|
+
start: searched.start,
|
|
168
|
+
end: Math.min(searched.end, offset)
|
|
169
|
+
};
|
|
170
|
+
const lineStartsView = source.lineStartsView;
|
|
171
|
+
let last = lineStartsView.indexForOffset(range.end);
|
|
172
|
+
while (last >= 0) {
|
|
173
|
+
const reach = Math.max(range.start, (lineStartsView.at(last) ?? 0) - FIND_WINDOW_UNITS);
|
|
174
|
+
const first = lineStartsView.indexForOffset(reach);
|
|
175
|
+
const firstStart = lineStartsView.at(first) ?? 0;
|
|
176
|
+
const window = {
|
|
177
|
+
start: Math.max(range.start, firstStart),
|
|
178
|
+
end: Math.min(range.end, lineEndAt(source, last))
|
|
179
|
+
};
|
|
180
|
+
const found = window.end < window.start ? null : lastMatchInWindow(source, query, window, offset, options);
|
|
181
|
+
if (found) return found;
|
|
182
|
+
if (firstStart <= range.start) return null;
|
|
183
|
+
last = first - 1;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function lastMatchInWindow(source, query, range, offset, options) {
|
|
188
|
+
let last = null;
|
|
189
|
+
let beforeLast = null;
|
|
190
|
+
scanRange(source, query, range, options.captureMatches ?? false, (start, end, captures) => {
|
|
191
|
+
if (end > offset) return false;
|
|
192
|
+
beforeLast = last;
|
|
193
|
+
last = {
|
|
194
|
+
start,
|
|
195
|
+
end,
|
|
196
|
+
matches: captures
|
|
197
|
+
};
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
if (!last) return null;
|
|
201
|
+
if (!escapesEmptyMatch(last, offset, options.escapeEmptyMatchAtOffset ?? false)) return last;
|
|
202
|
+
return beforeLast;
|
|
203
|
+
}
|
|
204
|
+
function nextCodePointOffset(source, offset) {
|
|
205
|
+
const text = source.readRange(offset, Math.min(source.length, offset + 2));
|
|
206
|
+
return offset + Math.max(1, codePointSizeAt(text, 0));
|
|
207
|
+
}
|
|
208
|
+
function queryIsCaseSensitiveByContent(searchString) {
|
|
209
|
+
return searchString.toLowerCase() !== searchString.toUpperCase();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether every part of a pattern is provably unable to match a line break.
|
|
213
|
+
*
|
|
214
|
+
* Asked the safe way round. Listing the constructs that CAN hold a break means
|
|
215
|
+
* one nobody thought of routes a query to the windowed path, where it answers
|
|
216
|
+
* "no results" for text that is plainly there — the one wrong answer a search
|
|
217
|
+
* must never give. So a query is windowed only when every part of it is provably
|
|
218
|
+
* break-free, and anything unrecognized takes the slower path that is always
|
|
219
|
+
* correct.
|
|
220
|
+
*/
|
|
221
|
+
function isLineSafePattern(pattern) {
|
|
222
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
223
|
+
const char = pattern[index];
|
|
224
|
+
if (char === "\n") return false;
|
|
225
|
+
if (char === "[") {
|
|
226
|
+
const end = characterClassEnd(pattern, index);
|
|
227
|
+
if (end === null || !isLineSafeCharacterClass(pattern.slice(index + 1, end))) return false;
|
|
228
|
+
index = end;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === "(" && !isLineSafeGroupOpener(pattern, index)) return false;
|
|
232
|
+
if (char !== "\\") continue;
|
|
233
|
+
index += 1;
|
|
234
|
+
const escaped = pattern[index];
|
|
235
|
+
if (escaped === void 0) return false;
|
|
236
|
+
if (!LINE_SAFE_ESCAPES.has(escaped) && /[\p{L}\p{N}]/u.test(escaped)) return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Only a group that bundles what is written inside it, leaving its meaning alone.
|
|
242
|
+
*
|
|
243
|
+
* A capture, a non-capture and either lookaround are all of those. A modifier
|
|
244
|
+
* group is not: it can switch on the flag under which a dot spans a break, so the
|
|
245
|
+
* characters inside it stop saying what they say anywhere else — and reading back
|
|
246
|
+
* which flags one names is more machinery than routing it to the correct path
|
|
247
|
+
* costs. Every other spelling is one nobody here recognized, which is the same
|
|
248
|
+
* answer.
|
|
249
|
+
*/
|
|
250
|
+
function isLineSafeGroupOpener(pattern, index) {
|
|
251
|
+
if (pattern[index + 1] !== "?") return true;
|
|
252
|
+
const kind = pattern[index + 2];
|
|
253
|
+
return kind === ":" || kind === "=" || kind === "!" || kind === "<";
|
|
254
|
+
}
|
|
255
|
+
function isLineSafeCharacterClass(body) {
|
|
256
|
+
if (body.startsWith("^")) return false;
|
|
257
|
+
return !body.includes("\\") && !body.includes("\n");
|
|
258
|
+
}
|
|
259
|
+
function characterClassEnd(pattern, from) {
|
|
260
|
+
for (let index = from + 1; index < pattern.length; index += 1) if (pattern[index] === "\\") index += 1;
|
|
261
|
+
else if (pattern[index] === "]") return index;
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
function compileFindQuery(query) {
|
|
265
|
+
if (query.searchString.length === 0) return null;
|
|
266
|
+
const source = query.isRegex ? query.searchString : escapeRegExpCharacters(query.searchString);
|
|
267
|
+
const plan = planFindQuery(query);
|
|
268
|
+
const lineCrossing = plan.kind === "range" || plan.kind === "literal-lines";
|
|
269
|
+
const flags = `g${query.matchCase ? "" : "i"}${lineCrossing ? "m" : ""}`;
|
|
270
|
+
const canUseSimpleSearch = !query.isRegex && (query.matchCase || !queryIsCaseSensitiveByContent(query.searchString));
|
|
271
|
+
const regex = compileRegex(source, flags);
|
|
272
|
+
if (!regex) return null;
|
|
273
|
+
return {
|
|
274
|
+
regex,
|
|
275
|
+
simpleSearch: canUseSimpleSearch ? query.searchString : null,
|
|
276
|
+
wholeWord: query.wholeWord,
|
|
277
|
+
plan
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function planFindQuery(query) {
|
|
281
|
+
if (!query.isRegex) {
|
|
282
|
+
const lineBreaks = query.searchString.split("\n").length - 1;
|
|
283
|
+
return lineBreaks === 0 ? { kind: "lines" } : {
|
|
284
|
+
kind: "literal-lines",
|
|
285
|
+
lineBreaks
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (!isLineSafePattern(query.searchString)) return {
|
|
289
|
+
kind: "range",
|
|
290
|
+
reason: "pattern-may-match-line-break"
|
|
291
|
+
};
|
|
292
|
+
return hasLineAnchor(query.searchString) ? { kind: "anchored-lines" } : { kind: "lines" };
|
|
293
|
+
}
|
|
294
|
+
function hasLineAnchor(pattern) {
|
|
295
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
296
|
+
const char = pattern[index];
|
|
297
|
+
if (char === "\\") index += 1;
|
|
298
|
+
else if (char === "[") index = characterClassEnd(pattern, index) ?? pattern.length;
|
|
299
|
+
else if (char === "^" || char === "$") return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The pattern under the unicode flag where it takes it, and without it where it
|
|
305
|
+
* does not.
|
|
306
|
+
*
|
|
307
|
+
* Wanted, because `u` is what makes a surrogate pair one character to a dot and
|
|
308
|
+
* `\p{...}` mean anything at all. Not required, because its grammar rejects
|
|
309
|
+
* spellings the flagless one accepts — an identity escape of a character that
|
|
310
|
+
* needs none (`\-`, `\ `), a brace or bracket standing for itself, a legacy
|
|
311
|
+
* back-reference — and every one of those is what escaping a search by hand
|
|
312
|
+
* produces. A rejected pattern is not reported anywhere the user can see it: it
|
|
313
|
+
* reaches them as "No results", indistinguishable from text that is not there.
|
|
314
|
+
*/
|
|
315
|
+
function compileRegex(source, flags) {
|
|
316
|
+
try {
|
|
317
|
+
return new RegExp(source, `${flags}u`);
|
|
318
|
+
} catch {}
|
|
319
|
+
try {
|
|
320
|
+
return new RegExp(source, flags);
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function searchRanges(source, ranges) {
|
|
326
|
+
if (!ranges || ranges.length === 0) return [{
|
|
327
|
+
start: 0,
|
|
328
|
+
end: source.length
|
|
329
|
+
}];
|
|
330
|
+
const clamped = ranges.map((range) => ({
|
|
331
|
+
start: clampSourceOffset(source, range.start),
|
|
332
|
+
end: clampSourceOffset(source, range.end)
|
|
333
|
+
})).filter((range) => range.start <= range.end).toSorted(compareTextOffsetRanges);
|
|
334
|
+
const merged = [];
|
|
335
|
+
for (const range of clamped) {
|
|
336
|
+
const previous = merged.at(-1);
|
|
337
|
+
if (previous && range.start <= previous.end) {
|
|
338
|
+
merged[merged.length - 1] = {
|
|
339
|
+
start: previous.start,
|
|
340
|
+
end: Math.max(previous.end, range.end)
|
|
341
|
+
};
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
merged.push(range);
|
|
345
|
+
}
|
|
346
|
+
return merged;
|
|
347
|
+
}
|
|
348
|
+
function clampSourceOffset(source, offset) {
|
|
349
|
+
return Math.min(Math.max(0, offset), source.length);
|
|
350
|
+
}
|
|
351
|
+
/** False once the range is spent or the visitor stopped: nothing later in it is read. */
|
|
352
|
+
function scanRange(source, query, range, captureMatches, visit) {
|
|
353
|
+
if (query.plan.kind === "range") return scanWholeRange(source, query, range, captureMatches, visit);
|
|
354
|
+
if (query.plan.kind === "literal-lines") return scanSpanningLiteral(source, query, range, query.plan.lineBreaks, visit);
|
|
355
|
+
return scanLines(source, query, range, captureMatches, visit);
|
|
356
|
+
}
|
|
357
|
+
function scanLines(source, query, range, captureMatches, visit) {
|
|
358
|
+
const lineStartsView = source.lineStartsView;
|
|
359
|
+
let index = lineStartsView.indexForOffset(range.start);
|
|
360
|
+
while (index < lineStartsView.length) {
|
|
361
|
+
const start = lineStartsView.at(index);
|
|
362
|
+
if (start === void 0 || start > range.end) return true;
|
|
363
|
+
const last = lastWindowLine(source, index, start, range.end);
|
|
364
|
+
if (!scanLineWindow(readWindow(source, start, lineEndAt(source, last), range), query, range, captureMatches, visit)) return false;
|
|
365
|
+
index = last + 1;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
function lastWindowLine(source, first, start, rangeEnd) {
|
|
370
|
+
const reach = Math.min(rangeEnd, start + FIND_WINDOW_UNITS);
|
|
371
|
+
return Math.max(first, source.lineStartsView.indexForOffset(reach));
|
|
372
|
+
}
|
|
373
|
+
function scanLineWindow(window, query, range, captureMatches, visit) {
|
|
374
|
+
if (query.plan.kind !== "anchored-lines") return scanWindow(window, query, range, captureMatches, visit);
|
|
375
|
+
let lineStart = 0;
|
|
376
|
+
while (lineStart <= window.text.length) {
|
|
377
|
+
const found = window.text.indexOf("\n", lineStart);
|
|
378
|
+
const lineEnd = found === -1 ? window.text.length : found;
|
|
379
|
+
const line = {
|
|
380
|
+
text: window.text.slice(lineStart, lineEnd),
|
|
381
|
+
start: window.start + lineStart,
|
|
382
|
+
from: Math.max(0, window.from - lineStart)
|
|
383
|
+
};
|
|
384
|
+
if (lineEnd >= window.from && !scanWindow(line, query, range, captureMatches, visit)) return false;
|
|
385
|
+
lineStart = lineEnd + 1;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function scanSpanningLiteral(source, query, range, lineBreaks, visit) {
|
|
390
|
+
const lineStartsView = source.lineStartsView;
|
|
391
|
+
const finalLine = lineStartsView.indexForOffset(range.end);
|
|
392
|
+
let first = lineStartsView.indexForOffset(range.start);
|
|
393
|
+
const cursor = { offset: range.start };
|
|
394
|
+
while (first <= finalLine) {
|
|
395
|
+
const start = lineStartsView.at(first) ?? 0;
|
|
396
|
+
const last = Math.min(finalLine, Math.max(first + lineBreaks, lastWindowLine(source, first, start, range.end)));
|
|
397
|
+
if (!scanWindow({
|
|
398
|
+
text: source.readRange(start, Math.max(start, lineEndAt(source, last))),
|
|
399
|
+
start,
|
|
400
|
+
from: Math.max(0, cursor.offset - start)
|
|
401
|
+
}, query, range, false, visit, cursor)) return false;
|
|
402
|
+
if (last === finalLine) return true;
|
|
403
|
+
first = last + 1 - lineBreaks;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
function scanWholeRange(source, query, range, captureMatches, visit) {
|
|
408
|
+
const lineStartsView = source.lineStartsView;
|
|
409
|
+
const start = lineStartsView.at(lineStartsView.indexForOffset(range.start)) ?? 0;
|
|
410
|
+
const end = lineEndAt(source, lineStartsView.indexForOffset(range.end));
|
|
411
|
+
return scanWindow(readWindow(source, start, Math.max(start, end), range), query, range, captureMatches, visit);
|
|
412
|
+
}
|
|
413
|
+
function readWindow(source, start, end, range) {
|
|
414
|
+
return {
|
|
415
|
+
text: source.readRange(start, end),
|
|
416
|
+
start,
|
|
417
|
+
from: Math.max(0, range.start - start)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** False once the range is spent or the visitor stopped, so no later window can add anything. */
|
|
421
|
+
function scanWindow(window, query, range, captureMatches, visit, cursor) {
|
|
422
|
+
if (query.simpleSearch && !captureMatches) return scanSimpleMatches(window, query.simpleSearch, query.wholeWord, range, visit, cursor);
|
|
423
|
+
return scanRegexMatches(window, query, range, captureMatches, visit, cursor);
|
|
424
|
+
}
|
|
425
|
+
function scanSimpleMatches(window, searchString, wholeWord, range, visit, cursor) {
|
|
426
|
+
let index = window.text.indexOf(searchString, window.from);
|
|
427
|
+
while (index !== -1) {
|
|
428
|
+
const start = window.start + index;
|
|
429
|
+
const end = start + searchString.length;
|
|
430
|
+
if (end > range.end) return false;
|
|
431
|
+
if (cursor) cursor.offset = end;
|
|
432
|
+
if (validWholeWordMatch(window.text, index, searchString.length, wholeWord)) {
|
|
433
|
+
if (!visit(start, end, null)) return false;
|
|
434
|
+
}
|
|
435
|
+
index = window.text.indexOf(searchString, index + searchString.length);
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
function scanRegexMatches(window, query, range, captureMatches, visit, cursor) {
|
|
440
|
+
query.regex.lastIndex = window.from;
|
|
441
|
+
while (true) {
|
|
442
|
+
const match = query.regex.exec(window.text);
|
|
443
|
+
if (!match) return true;
|
|
444
|
+
const start = window.start + match.index;
|
|
445
|
+
const end = start + match[0].length;
|
|
446
|
+
if (start > range.end || end > range.end) return false;
|
|
447
|
+
if (cursor) cursor.offset = end;
|
|
448
|
+
if (validWholeWordMatch(window.text, match.index, match[0].length, query.wholeWord)) {
|
|
449
|
+
if (!visit(start, end, captureMatches ? match : null)) return false;
|
|
450
|
+
}
|
|
451
|
+
if (match[0].length === 0) advancePastEmptyMatch(query.regex, window.text);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function advancePastEmptyMatch(regex, text) {
|
|
455
|
+
const current = regex.lastIndex;
|
|
456
|
+
if (current > text.length) return;
|
|
457
|
+
const size = codePointSizeAt(text, current);
|
|
458
|
+
regex.lastIndex = current + Math.max(1, size);
|
|
459
|
+
}
|
|
460
|
+
function validWholeWordMatch(text, start, length, wholeWord) {
|
|
461
|
+
if (!wholeWord) return true;
|
|
462
|
+
return isWholeWordRange(text, {
|
|
463
|
+
start,
|
|
464
|
+
end: start + length
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
export { FIND_MATCHES_LIMIT, FIND_REPLACE_ALL_LIMIT, FIND_WINDOW_UNITS, countMatches, escapeRegExpCharacters, findLineRange, findMatchIndex, findMatches, findNextMatchFrom, findPreviousMatchFrom, findTextSourceFromSnapshot };
|
|
469
|
+
|
|
470
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["import {\n codePointSizeAt,\n compareTextOffsetRanges,\n isWholeWordRange,\n type TextOffsetRange,\n type TextReadSnapshot,\n} from '@singapore-editor/core/document'\n\n// Bounds the match set the widget counts and paints, so a pathological query\n// on a huge file cannot stall the frame.\nexport const FIND_MATCHES_LIMIT = 19_999\n\n// Replace All must never apply to a truncated match set: stopping early would\n// silently rewrite part of the document and leave the rest, with no signal to\n// the user. Searching is linear, so the only real cost of lifting the cap here\n// is the edit batch itself.\nexport const FIND_REPLACE_ALL_LIMIT = 1_073_741_824\n\n// How much text a windowed scan reads at once, rounded up to whole lines. Large\n// enough that the tree descent per read is noise, small enough that a fragmented\n// document is never joined into one string the size of the file.\nexport const FIND_WINDOW_UNITS = 65_536\n\n// Escapes whose class cannot hold a line break, so a pattern built only from\n// these can be answered a line at a time. A carriage return is among them: it\n// sits inside a line, not between two.\nconst LINE_SAFE_ESCAPES = new Set(['d', 'w', 't', 'r', 'f', 'v', 'b', 'B', '0'])\n\nexport type FindRange = TextOffsetRange\n\n// Read-only line-start access, structurally the view a document snapshot\n// already hands out, so a search never asks for the array behind it.\nexport type FindLineStartsView = {\n readonly length: number\n at(index: number): number | undefined\n indexForOffset(offset: number): number\n}\n\n/**\n * The text a search reads, in ranges rather than as one string.\n *\n * A query that cannot match a line break is answered a line at a time, so a\n * keystroke in the find box costs the lines examined instead of a copy of the\n * whole document — and a chunked buffer gets searched where it lies.\n */\nexport type FindTextSource = {\n readonly length: number\n readRange(start: number, end: number): string\n readonly lineStartsView: FindLineStartsView\n}\n\nexport function findTextSourceFromSnapshot(snapshot: TextReadSnapshot): FindTextSource {\n return {\n length: snapshot.length,\n readRange: (start, end) => snapshot.readRange(start, end),\n lineStartsView: {\n length: snapshot.lineCount,\n at: (index) =>\n index < 0 || index >= snapshot.lineCount ? undefined : snapshot.lineStart(index),\n indexForOffset: (offset) => snapshot.lineAt(offset),\n },\n }\n}\n\nexport type FindQuery = {\n readonly searchString: string\n readonly isRegex: boolean\n readonly matchCase: boolean\n readonly wholeWord: boolean\n}\n\nexport type FindMatch = FindRange & {\n readonly matches: readonly string[] | null\n}\n\nexport type FindMatchFromOptions = {\n readonly captureMatches?: boolean\n readonly loop?: boolean\n readonly escapeEmptyMatchAtOffset?: boolean\n}\n\n/** What the two directions have in common, so one caller can hold either. */\nexport type FindMatchFromSearcher = (\n source: FindTextSource,\n query: FindQuery,\n offset: number,\n ranges: readonly FindRange[] | null,\n options: FindMatchFromOptions,\n) => FindMatch | null\n\ntype CompiledFindQuery = {\n readonly regex: RegExp\n // Non-null only when a plain indexOf over the untouched text is exactly\n // equivalent to the regex; see compileFindQuery.\n readonly simpleSearch: string | null\n readonly wholeWord: boolean\n readonly plan: FindQueryPlan\n}\n\n/**\n * How much text one match can depend on, which is what decides how it is read.\n *\n * `lines` and `anchored-lines` cannot match a break, so they are read in windows\n * of whole lines. `literal-lines` is a literal holding breaks: it spans a known\n * number of lines, so it is windowed too. `range` is every pattern nothing here\n * can bound, read as one window because that is the only reading always correct.\n */\nexport type FindQueryPlan =\n | { readonly kind: 'lines' }\n | { readonly kind: 'anchored-lines' }\n | { readonly kind: 'literal-lines'; readonly lineBreaks: number }\n | { readonly kind: 'range'; readonly reason: 'pattern-may-match-line-break' }\n\n// One stretch of the document already read: the text of\n// `[start, start + text.length)`, to be searched from `from` onward.\ntype FindWindow = {\n readonly text: string\n readonly start: number\n readonly from: number\n}\n\nexport function findMatches(\n source: FindTextSource,\n query: FindQuery,\n ranges: readonly FindRange[] | null = null,\n captureMatches = false,\n limit = FIND_MATCHES_LIMIT,\n): readonly FindMatch[] {\n const compiled = compileFindQuery(query)\n if (!compiled || limit <= 0) return []\n\n const matches: FindMatch[] = []\n const collect: FindMatchVisitor = (start, end, captures) => {\n matches.push({ start, end, matches: captures })\n return matches.length < limit\n }\n for (const range of searchRanges(source, ranges)) {\n scanRange(source, compiled, range, captureMatches, collect)\n if (matches.length >= limit) break\n }\n\n return matches\n}\n\n/**\n * Every match counted, none kept: a total costs no record per match.\n *\n * With `before`, only the matches starting in front of that offset, which is the\n * position of a match the listing stopped short of.\n */\nexport function countMatches(\n source: FindTextSource,\n query: FindQuery,\n ranges: readonly FindRange[] | null = null,\n before = Number.POSITIVE_INFINITY,\n): number {\n const compiled = compileFindQuery(query)\n if (!compiled) return 0\n\n let count = 0\n const tally: FindMatchVisitor = (start) => {\n if (start >= before) return false\n\n count += 1\n return true\n }\n for (const range of searchRanges(source, ranges)) {\n if (range.start >= before) break\n scanRange(source, compiled, range, false, tally)\n }\n\n return count\n}\n\n/** Null for a query that cannot be compiled, which is one that finds nothing. */\nexport function findQueryPlan(query: FindQuery): FindQueryPlan | null {\n return compileFindQuery(query)?.plan ?? null\n}\n\n/** The line holding `offset`, break excluded. */\nexport function findLineRange(source: FindTextSource, offset: number): FindRange {\n const index = source.lineStartsView.indexForOffset(offset)\n return { start: source.lineStartsView.at(index) ?? 0, end: lineEndAt(source, index) }\n}\n\n/**\n * One match, scanned for from an offset instead of picked out of a list.\n *\n * A listing stops at FIND_MATCHES_LIMIT, and past that every entry in it sits\n * behind the cursor: picking from it answers each press with the top of the\n * document. Scanning outward stops at the first match it reaches, so how far\n * navigation can go is not what a paint budget allowed.\n */\nexport function findNextMatchFrom(\n source: FindTextSource,\n query: FindQuery,\n offset: number,\n ranges: readonly FindRange[] | null = null,\n options: FindMatchFromOptions = {},\n): FindMatch | null {\n const compiled = compileFindQuery(query)\n if (!compiled) return null\n\n const searched = searchRanges(source, ranges)\n const from = clampSourceOffset(source, offset)\n const ahead = firstMatchAfter(source, compiled, searched, from, options)\n if (ahead) return ahead\n if (options.loop === false) return null\n\n // Wrapping is a second bounded scan rather than a listing, so a cursor past\n // the last match still costs only the text up to the first one.\n return firstMatchAtOrAfter(source, compiled, searched, searched[0]?.start ?? 0, options)\n}\n\nexport function findPreviousMatchFrom(\n source: FindTextSource,\n query: FindQuery,\n offset: number,\n ranges: readonly FindRange[] | null = null,\n options: FindMatchFromOptions = {},\n): FindMatch | null {\n const compiled = compileFindQuery(query)\n if (!compiled) return null\n\n const searched = searchRanges(source, ranges)\n const from = clampSourceOffset(source, offset)\n const behind = lastMatchBefore(source, compiled, searched, from, options)\n if (behind) return behind\n if (options.loop === false) return null\n\n return lastMatchBefore(source, compiled, searched, source.length, {\n ...options,\n escapeEmptyMatchAtOffset: false,\n })\n}\n\nexport function findMatchIndex(matches: readonly FindMatch[], range: FindRange): number {\n return matches.findIndex((match) => match.start === range.start && match.end === range.end)\n}\n\nexport function escapeRegExpCharacters(value: string): string {\n return value.replace(/[\\\\{}*+?|^$.[\\]()]/g, '\\\\$&')\n}\n\n// The break belongs to no line: a query that cannot match one would otherwise be\n// handed a haystack whose `$` sits behind it.\nfunction lineEndAt(source: FindTextSource, index: number): number {\n const nextLineStart = source.lineStartsView.at(index + 1)\n return nextLineStart === undefined ? source.length : nextLineStart - 1\n}\n\nfunction escapesEmptyMatch(match: FindMatch, offset: number, escape: boolean): boolean {\n return escape && match.start === match.end && match.start === offset\n}\n\nfunction firstMatchAfter(\n source: FindTextSource,\n query: CompiledFindQuery,\n ranges: readonly FindRange[],\n offset: number,\n options: FindMatchFromOptions,\n): FindMatch | null {\n const found = firstMatchAtOrAfter(source, query, ranges, offset, options)\n if (!found) return null\n if (!escapesEmptyMatch(found, offset, options.escapeEmptyMatchAtOffset ?? false)) return found\n\n // Resumed past the offset itself, once: whatever stands there is the entry\n // after the one being escaped, empty or not.\n return firstMatchAtOrAfter(source, query, ranges, nextCodePointOffset(source, offset), options)\n}\n\nfunction firstMatchAtOrAfter(\n source: FindTextSource,\n query: CompiledFindQuery,\n ranges: readonly FindRange[],\n offset: number,\n options: FindMatchFromOptions,\n): FindMatch | null {\n for (const range of ranges) {\n if (range.end < offset) continue\n\n const found = firstMatchInRange(source, query, range, offset, options.captureMatches ?? false)\n if (found) return found\n }\n\n return null\n}\n\n// The range is never cut at the offset: a pattern resumed there simply begins a\n// fresh match at the cursor — 'pha' inside 'alpha' for `\\w+` — and a range no\n// listing of the document holds is one the reader was never shown and Replace\n// must never rewrite. What the offset decides is which matches are kept, not\n// where the scan starts. Uncapped, because what stands at the cursor is being\n// asked for regardless of where the paint budget ran out.\nfunction firstMatchInRange(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n offset: number,\n captureMatches: boolean,\n): FindMatch | null {\n let found: FindMatch | null = null\n const scanned = { start: forwardScanStart(source, query, range, offset), end: range.end }\n scanRange(source, query, scanned, captureMatches, (start, end, captures) => {\n if (start < offset) return true\n\n found = { start, end, matches: captures }\n return false\n })\n\n return found\n}\n\n// Lines in front of the cursor are skipped rather than searched: a match that\n// cannot hold a break cannot reach out of the line it starts on, so none of them\n// could answer at or after the offset anyway.\nfunction forwardScanStart(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n offset: number,\n): number {\n if (!isLineLocalPlan(query.plan)) return range.start\n\n const lineStartsView = source.lineStartsView\n const line = lineStartsView.indexForOffset(Math.max(range.start, offset))\n return Math.max(range.start, lineStartsView.at(line) ?? 0)\n}\n\nfunction isLineLocalPlan(plan: FindQueryPlan): boolean {\n return plan.kind === 'lines' || plan.kind === 'anchored-lines'\n}\n\nfunction lastMatchBefore(\n source: FindTextSource,\n query: CompiledFindQuery,\n ranges: readonly FindRange[],\n offset: number,\n options: FindMatchFromOptions,\n): FindMatch | null {\n for (const range of ranges.toReversed()) {\n if (range.start > offset) continue\n\n const found = lastMatchInRange(source, query, range, offset, options)\n if (found) return found\n }\n\n return null\n}\n\n// Walked back a window at a time, because the match before the cursor is the one\n// being asked for and listing everything behind it to reach the last entry is\n// the whole document on a search that started near its end.\nfunction lastMatchInRange(\n source: FindTextSource,\n query: CompiledFindQuery,\n searched: FindRange,\n offset: number,\n options: FindMatchFromOptions,\n): FindMatch | null {\n // A pattern that can run past a break is shown the text behind the cursor too:\n // cut at the cursor's line, `[^a]+$` would end there on a match no listing of\n // the document holds. The scan still stops at the first match past the cursor.\n if (!isLineLocalPlan(query.plan))\n return lastMatchInWindow(source, query, searched, offset, options)\n\n const range = { start: searched.start, end: Math.min(searched.end, offset) }\n const lineStartsView = source.lineStartsView\n let last = lineStartsView.indexForOffset(range.end)\n while (last >= 0) {\n const reach = Math.max(range.start, (lineStartsView.at(last) ?? 0) - FIND_WINDOW_UNITS)\n const first = lineStartsView.indexForOffset(reach)\n const firstStart = lineStartsView.at(first) ?? 0\n const window = {\n start: Math.max(range.start, firstStart),\n end: Math.min(range.end, lineEndAt(source, last)),\n }\n const found =\n window.end < window.start ? null : lastMatchInWindow(source, query, window, offset, options)\n if (found) return found\n if (firstStart <= range.start) return null\n\n last = first - 1\n }\n\n return null\n}\n\n// The last match in one window, escaping the same way stepping an ordered list\n// does: what stands before a zero-width match parked on the cursor is the entry\n// the search is being asked for, and it may be in an earlier window. Uncapped,\n// and only the last two are kept: a listing that stopped at the paint cap would\n// answer every press behind the cursor with the 19,999th match.\nfunction lastMatchInWindow(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n offset: number,\n options: FindMatchFromOptions,\n): FindMatch | null {\n let last: FindMatch | null = null\n let beforeLast: FindMatch | null = null\n scanRange(source, query, range, options.captureMatches ?? false, (start, end, captures) => {\n if (end > offset) return false\n\n beforeLast = last\n last = { start, end, matches: captures }\n return true\n })\n if (!last) return null\n if (!escapesEmptyMatch(last, offset, options.escapeEmptyMatchAtOffset ?? false)) return last\n\n return beforeLast\n}\n\n// A pattern compiled with the unicode flag may not be resumed inside a surrogate\n// pair, so stepping off an empty match steps a code point rather than a unit.\nfunction nextCodePointOffset(source: FindTextSource, offset: number): number {\n const text = source.readRange(offset, Math.min(source.length, offset + 2))\n return offset + Math.max(1, codePointSizeAt(text, 0))\n}\n\n// True when the query contains at least one character whose case can differ,\n// which is the only situation where matchCase changes what a plain search\n// finds. Digits, punctuation and CJK are caseless, so an untouched indexOf is\n// still exact for them even with matchCase off.\nfunction queryIsCaseSensitiveByContent(searchString: string): boolean {\n return searchString.toLowerCase() !== searchString.toUpperCase()\n}\n\n/**\n * Whether every part of a pattern is provably unable to match a line break.\n *\n * Asked the safe way round. Listing the constructs that CAN hold a break means\n * one nobody thought of routes a query to the windowed path, where it answers\n * \"no results\" for text that is plainly there — the one wrong answer a search\n * must never give. So a query is windowed only when every part of it is provably\n * break-free, and anything unrecognized takes the slower path that is always\n * correct.\n */\nfunction isLineSafePattern(pattern: string): boolean {\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n if (char === '\\n') return false\n if (char === '[') {\n const end = characterClassEnd(pattern, index)\n if (end === null || !isLineSafeCharacterClass(pattern.slice(index + 1, end))) return false\n\n index = end\n continue\n }\n\n if (char === '(' && !isLineSafeGroupOpener(pattern, index)) return false\n if (char !== '\\\\') continue\n\n index += 1\n const escaped = pattern[index]\n if (escaped === undefined) return false\n // A hex or unicode escape can spell the break itself, and reading which one\n // it spells is more machinery than routing it to the correct path costs.\n if (!LINE_SAFE_ESCAPES.has(escaped) && /[\\p{L}\\p{N}]/u.test(escaped)) return false\n }\n\n return true\n}\n\n/**\n * Only a group that bundles what is written inside it, leaving its meaning alone.\n *\n * A capture, a non-capture and either lookaround are all of those. A modifier\n * group is not: it can switch on the flag under which a dot spans a break, so the\n * characters inside it stop saying what they say anywhere else — and reading back\n * which flags one names is more machinery than routing it to the correct path\n * costs. Every other spelling is one nobody here recognized, which is the same\n * answer.\n */\nfunction isLineSafeGroupOpener(pattern: string, index: number): boolean {\n if (pattern[index + 1] !== '?') return true\n\n const kind = pattern[index + 2]\n // '<' opens a lookbehind or names a capture, and neither changes what matches.\n return kind === ':' || kind === '=' || kind === '!' || kind === '<'\n}\n\n// Only a positive class naming its members outright: a negated one holds a break\n// unless it names it, and an escape inside one can bring a whole class with it.\nfunction isLineSafeCharacterClass(body: string): boolean {\n if (body.startsWith('^')) return false\n return !body.includes('\\\\') && !body.includes('\\n')\n}\n\nfunction characterClassEnd(pattern: string, from: number): number | null {\n for (let index = from + 1; index < pattern.length; index += 1) {\n if (pattern[index] === '\\\\') index += 1\n else if (pattern[index] === ']') return index\n }\n\n return null\n}\n\nfunction compileFindQuery(query: FindQuery): CompiledFindQuery | null {\n if (query.searchString.length === 0) return null\n\n const source = query.isRegex ? query.searchString : escapeRegExpCharacters(query.searchString)\n const plan = planFindQuery(query)\n const lineCrossing = plan.kind === 'range' || plan.kind === 'literal-lines'\n // Flags describe the haystack, and only a line-crossing search is ever handed\n // one holding a break: a per-line slice bounds `^` and `$` by itself, so `m`\n // there would only claim breaks the slice does not contain.\n const flags = `g${query.matchCase ? '' : 'i'}${lineCrossing ? 'm' : ''}`\n\n // The fast path may never case-fold the haystack: folding is not\n // length-preserving (U+0130 'İ' lowercases to two code units), so an index\n // into folded text does not address the original text, and a match reported\n // from it slices the wrong characters — Replace would then overwrite them.\n // Fall back to the case-insensitive regex, which reports original indices.\n const canUseSimpleSearch =\n !query.isRegex && (query.matchCase || !queryIsCaseSensitiveByContent(query.searchString))\n\n const regex = compileRegex(source, flags)\n if (!regex) return null\n\n return {\n regex,\n simpleSearch: canUseSimpleSearch ? query.searchString : null,\n wholeWord: query.wholeWord,\n plan,\n }\n}\n\nfunction planFindQuery(query: FindQuery): FindQueryPlan {\n if (!query.isRegex) {\n const lineBreaks = query.searchString.split('\\n').length - 1\n return lineBreaks === 0 ? { kind: 'lines' } : { kind: 'literal-lines', lineBreaks }\n }\n\n if (!isLineSafePattern(query.searchString))\n return { kind: 'range', reason: 'pattern-may-match-line-break' }\n return hasLineAnchor(query.searchString) ? { kind: 'anchored-lines' } : { kind: 'lines' }\n}\n\n// Asked of a pattern already known to be line-safe, so a class cannot be negated\n// and a `^` inside one is a member rather than an anchor.\nfunction hasLineAnchor(pattern: string): boolean {\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n if (char === '\\\\') index += 1\n else if (char === '[') index = characterClassEnd(pattern, index) ?? pattern.length\n else if (char === '^' || char === '$') return true\n }\n\n return false\n}\n\n/**\n * The pattern under the unicode flag where it takes it, and without it where it\n * does not.\n *\n * Wanted, because `u` is what makes a surrogate pair one character to a dot and\n * `\\p{...}` mean anything at all. Not required, because its grammar rejects\n * spellings the flagless one accepts — an identity escape of a character that\n * needs none (`\\-`, `\\ `), a brace or bracket standing for itself, a legacy\n * back-reference — and every one of those is what escaping a search by hand\n * produces. A rejected pattern is not reported anywhere the user can see it: it\n * reaches them as \"No results\", indistinguishable from text that is not there.\n */\nfunction compileRegex(source: string, flags: string): RegExp | null {\n try {\n return new RegExp(source, `${flags}u`)\n } catch {\n // Ignored: whether the pattern is invalid outright or only invalid under the\n // stricter grammar is what the second construction answers.\n }\n\n try {\n return new RegExp(source, flags)\n } catch {\n return null\n }\n}\n\nfunction searchRanges(\n source: FindTextSource,\n ranges: readonly FindRange[] | null,\n): readonly FindRange[] {\n if (!ranges || ranges.length === 0) return [{ start: 0, end: source.length }]\n\n const clamped = ranges\n .map((range) => ({\n start: clampSourceOffset(source, range.start),\n end: clampSourceOffset(source, range.end),\n }))\n .filter((range) => range.start <= range.end)\n .toSorted(compareTextOffsetRanges)\n\n // Scopes are followed through edits, and an edit spanning the seam between two\n // of them leaves them overlapping. Searching both would report the text they\n // share twice, which a Replace All then tries to rewrite twice.\n const merged: FindRange[] = []\n for (const range of clamped) {\n const previous = merged.at(-1)\n if (previous && range.start <= previous.end) {\n merged[merged.length - 1] = { start: previous.start, end: Math.max(previous.end, range.end) }\n continue\n }\n\n merged.push(range)\n }\n\n return merged\n}\n\nfunction clampSourceOffset(source: FindTextSource, offset: number): number {\n return Math.min(Math.max(0, offset), source.length)\n}\n\n// Visited in document order; false stops the scan where it stands.\ntype FindMatchVisitor = (start: number, end: number, captures: RegExpExecArray | null) => boolean\n\n/** False once the range is spent or the visitor stopped: nothing later in it is read. */\nfunction scanRange(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n): boolean {\n if (query.plan.kind === 'range')\n return scanWholeRange(source, query, range, captureMatches, visit)\n if (query.plan.kind === 'literal-lines')\n return scanSpanningLiteral(source, query, range, query.plan.lineBreaks, visit)\n\n return scanLines(source, query, range, captureMatches, visit)\n}\n\n// Whole lines, even where the range was cut inside one: the anchors and the word\n// boundaries have to answer for the line the reader sees rather than for where\n// the range happens to end, so the range only decides which matches are kept.\n//\n// Read many lines to a window. A match that cannot hold a break cannot leave its\n// line, and a break is no word character, so a pattern with no `^` or `$` finds in\n// the window exactly what it finds line by line, without a tree descent per line.\nfunction scanLines(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n): boolean {\n const lineStartsView = source.lineStartsView\n let index = lineStartsView.indexForOffset(range.start)\n while (index < lineStartsView.length) {\n const start = lineStartsView.at(index)\n if (start === undefined || start > range.end) return true\n\n const last = lastWindowLine(source, index, start, range.end)\n const window = readWindow(source, start, lineEndAt(source, last), range)\n if (!scanLineWindow(window, query, range, captureMatches, visit)) return false\n index = last + 1\n }\n\n return true\n}\n\nfunction lastWindowLine(\n source: FindTextSource,\n first: number,\n start: number,\n rangeEnd: number,\n): number {\n const reach = Math.min(rangeEnd, start + FIND_WINDOW_UNITS)\n return Math.max(first, source.lineStartsView.indexForOffset(reach))\n}\n\nfunction scanLineWindow(\n window: FindWindow,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n): boolean {\n if (query.plan.kind !== 'anchored-lines')\n return scanWindow(window, query, range, captureMatches, visit)\n\n // `^` and `$` answer for the ends of the haystack, so each line is its own.\n let lineStart = 0\n while (lineStart <= window.text.length) {\n const found = window.text.indexOf('\\n', lineStart)\n const lineEnd = found === -1 ? window.text.length : found\n const line = {\n text: window.text.slice(lineStart, lineEnd),\n start: window.start + lineStart,\n from: Math.max(0, window.from - lineStart),\n }\n if (lineEnd >= window.from && !scanWindow(line, query, range, captureMatches, visit))\n return false\n\n lineStart = lineEnd + 1\n }\n\n return true\n}\n\n// A literal holding breaks spans exactly that many lines past the one it starts\n// on, so windows overlapping by that many lines show every match whole to at\n// least one of them. The cursor is carried across: restarting the greedy scan at\n// an overlap's first character would change which of two overlapping candidates\n// wins, and with it every match after them.\nfunction scanSpanningLiteral(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n lineBreaks: number,\n visit: FindMatchVisitor,\n): boolean {\n const lineStartsView = source.lineStartsView\n const finalLine = lineStartsView.indexForOffset(range.end)\n let first = lineStartsView.indexForOffset(range.start)\n const cursor = { offset: range.start }\n while (first <= finalLine) {\n const start = lineStartsView.at(first) ?? 0\n const last = Math.min(\n finalLine,\n Math.max(first + lineBreaks, lastWindowLine(source, first, start, range.end)),\n )\n const text = source.readRange(start, Math.max(start, lineEndAt(source, last)))\n const window = { text, start, from: Math.max(0, cursor.offset - start) }\n if (!scanWindow(window, query, range, false, visit, cursor)) return false\n if (last === finalLine) return true\n\n first = last + 1 - lineBreaks\n }\n\n return true\n}\n\n// Listing every match of a pattern that can span the range's ends has to look at\n// every character between them, so the range is read as one window: a window\n// grown in steps would only re-run the pattern over text it already scanned. The\n// window still stops on line boundaries, or `^` and `$` would answer for its cut\n// ends instead of for real ones.\nfunction scanWholeRange(\n source: FindTextSource,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n): boolean {\n const lineStartsView = source.lineStartsView\n const start = lineStartsView.at(lineStartsView.indexForOffset(range.start)) ?? 0\n const end = lineEndAt(source, lineStartsView.indexForOffset(range.end))\n const window = readWindow(source, start, Math.max(start, end), range)\n return scanWindow(window, query, range, captureMatches, visit)\n}\n\nfunction readWindow(\n source: FindTextSource,\n start: number,\n end: number,\n range: FindRange,\n): FindWindow {\n return { text: source.readRange(start, end), start, from: Math.max(0, range.start - start) }\n}\n\n// Where the next raw match may begin. It moves past every raw match, including\n// one the whole-word filter rejects, so the scan keeps one non-overlap phase.\ntype FindCursor = { offset: number }\n\n/** False once the range is spent or the visitor stopped, so no later window can add anything. */\nfunction scanWindow(\n window: FindWindow,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n cursor?: FindCursor,\n): boolean {\n if (query.simpleSearch && !captureMatches)\n return scanSimpleMatches(window, query.simpleSearch, query.wholeWord, range, visit, cursor)\n\n return scanRegexMatches(window, query, range, captureMatches, visit, cursor)\n}\n\nfunction scanSimpleMatches(\n window: FindWindow,\n searchString: string,\n wholeWord: boolean,\n range: FindRange,\n visit: FindMatchVisitor,\n cursor?: FindCursor,\n): boolean {\n // Searched as-is: no folded copy of the text is allocated, and every index\n // returned addresses the window it came from.\n let index = window.text.indexOf(searchString, window.from)\n while (index !== -1) {\n const start = window.start + index\n const end = start + searchString.length\n if (end > range.end) return false\n if (cursor) cursor.offset = end\n if (validWholeWordMatch(window.text, index, searchString.length, wholeWord)) {\n if (!visit(start, end, null)) return false\n }\n\n index = window.text.indexOf(searchString, index + searchString.length)\n }\n\n return true\n}\n\nfunction scanRegexMatches(\n window: FindWindow,\n query: CompiledFindQuery,\n range: FindRange,\n captureMatches: boolean,\n visit: FindMatchVisitor,\n cursor?: FindCursor,\n): boolean {\n query.regex.lastIndex = window.from\n while (true) {\n const match = query.regex.exec(window.text)\n if (!match) return true\n\n const start = window.start + match.index\n const end = start + match[0].length\n if (start > range.end || end > range.end) return false\n if (cursor) cursor.offset = end\n if (validWholeWordMatch(window.text, match.index, match[0].length, query.wholeWord)) {\n if (!visit(start, end, captureMatches ? match : null)) return false\n }\n\n if (match[0].length === 0) advancePastEmptyMatch(query.regex, window.text)\n }\n}\n\nfunction advancePastEmptyMatch(regex: RegExp, text: string): void {\n const current = regex.lastIndex\n if (current > text.length) return\n\n const size = codePointSizeAt(text, current)\n regex.lastIndex = current + Math.max(1, size)\n}\n\nfunction validWholeWordMatch(\n text: string,\n start: number,\n length: number,\n wholeWord: boolean,\n): boolean {\n if (!wholeWord) return true\n return isWholeWordRange(text, { start, end: start + length })\n}\n"],"mappings":";;AAUA,IAAa,qBAAqB;AAMlC,IAAa,yBAAyB;AAKtC,IAAa,oBAAoB;AAKjC,IAAM,oCAAoB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG,CAAC;AAyB/E,SAAgB,2BAA2B,UAA4C;CACrF,OAAO;EACL,QAAQ,SAAS;EACjB,YAAY,OAAO,QAAQ,SAAS,UAAU,OAAO,GAAG;EACxD,gBAAgB;GACd,QAAQ,SAAS;GACjB,KAAK,UACH,QAAQ,KAAK,SAAS,SAAS,YAAY,KAAA,IAAY,SAAS,UAAU,KAAK;GACjF,iBAAiB,WAAW,SAAS,OAAO,MAAM;EACpD;CACF;AACF;AA2DA,SAAgB,YACd,QACA,OACA,SAAsC,MACtC,iBAAiB,OACjB,QAAQ,oBACc;CACtB,MAAM,WAAW,iBAAiB,KAAK;CACvC,IAAI,CAAC,YAAY,SAAS,GAAG,OAAO,CAAC;CAErC,MAAM,UAAuB,CAAC;CAC9B,MAAM,WAA6B,OAAO,KAAK,aAAa;EAC1D,QAAQ,KAAK;GAAE;GAAO;GAAK,SAAS;EAAS,CAAC;EAC9C,OAAO,QAAQ,SAAS;CAC1B;CACA,KAAK,MAAM,SAAS,aAAa,QAAQ,MAAM,GAAG;EAChD,UAAU,QAAQ,UAAU,OAAO,gBAAgB,OAAO;EAC1D,IAAI,QAAQ,UAAU,OAAO;CAC/B;CAEA,OAAO;AACT;;;;;;;AAQA,SAAgB,aACd,QACA,OACA,SAAsC,MACtC,SAAS,OAAO,mBACR;CACR,MAAM,WAAW,iBAAiB,KAAK;CACvC,IAAI,CAAC,UAAU,OAAO;CAEtB,IAAI,QAAQ;CACZ,MAAM,SAA2B,UAAU;EACzC,IAAI,SAAS,QAAQ,OAAO;EAE5B,SAAS;EACT,OAAO;CACT;CACA,KAAK,MAAM,SAAS,aAAa,QAAQ,MAAM,GAAG;EAChD,IAAI,MAAM,SAAS,QAAQ;EAC3B,UAAU,QAAQ,UAAU,OAAO,OAAO,KAAK;CACjD;CAEA,OAAO;AACT;;AAQA,SAAgB,cAAc,QAAwB,QAA2B;CAC/E,MAAM,QAAQ,OAAO,eAAe,eAAe,MAAM;CACzD,OAAO;EAAE,OAAO,OAAO,eAAe,GAAG,KAAK,KAAK;EAAG,KAAK,UAAU,QAAQ,KAAK;CAAE;AACtF;;;;;;;;;AAUA,SAAgB,kBACd,QACA,OACA,QACA,SAAsC,MACtC,UAAgC,CAAC,GACf;CAClB,MAAM,WAAW,iBAAiB,KAAK;CACvC,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,WAAW,aAAa,QAAQ,MAAM;CAE5C,MAAM,QAAQ,gBAAgB,QAAQ,UAAU,UADnC,kBAAkB,QAAQ,MACmB,GAAM,OAAO;CACvE,IAAI,OAAO,OAAO;CAClB,IAAI,QAAQ,SAAS,OAAO,OAAO;CAInC,OAAO,oBAAoB,QAAQ,UAAU,UAAU,SAAS,EAAE,EAAE,SAAS,GAAG,OAAO;AACzF;AAEA,SAAgB,sBACd,QACA,OACA,QACA,SAAsC,MACtC,UAAgC,CAAC,GACf;CAClB,MAAM,WAAW,iBAAiB,KAAK;CACvC,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,WAAW,aAAa,QAAQ,MAAM;CAE5C,MAAM,SAAS,gBAAgB,QAAQ,UAAU,UADpC,kBAAkB,QAAQ,MACoB,GAAM,OAAO;CACxE,IAAI,QAAQ,OAAO;CACnB,IAAI,QAAQ,SAAS,OAAO,OAAO;CAEnC,OAAO,gBAAgB,QAAQ,UAAU,UAAU,OAAO,QAAQ;EAChE,GAAG;EACH,0BAA0B;CAC5B,CAAC;AACH;AAEA,SAAgB,eAAe,SAA+B,OAA0B;CACtF,OAAO,QAAQ,WAAW,UAAU,MAAM,UAAU,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG;AAC5F;AAEA,SAAgB,uBAAuB,OAAuB;CAC5D,OAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD;AAIA,SAAS,UAAU,QAAwB,OAAuB;CAChE,MAAM,gBAAgB,OAAO,eAAe,GAAG,QAAQ,CAAC;CACxD,OAAO,kBAAkB,KAAA,IAAY,OAAO,SAAS,gBAAgB;AACvE;AAEA,SAAS,kBAAkB,OAAkB,QAAgB,QAA0B;CACrF,OAAO,UAAU,MAAM,UAAU,MAAM,OAAO,MAAM,UAAU;AAChE;AAEA,SAAS,gBACP,QACA,OACA,QACA,QACA,SACkB;CAClB,MAAM,QAAQ,oBAAoB,QAAQ,OAAO,QAAQ,QAAQ,OAAO;CACxE,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI,CAAC,kBAAkB,OAAO,QAAQ,QAAQ,4BAA4B,KAAK,GAAG,OAAO;CAIzF,OAAO,oBAAoB,QAAQ,OAAO,QAAQ,oBAAoB,QAAQ,MAAM,GAAG,OAAO;AAChG;AAEA,SAAS,oBACP,QACA,OACA,QACA,QACA,SACkB;CAClB,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,MAAM,QAAQ;EAExB,MAAM,QAAQ,kBAAkB,QAAQ,OAAO,OAAO,QAAQ,QAAQ,kBAAkB,KAAK;EAC7F,IAAI,OAAO,OAAO;CACpB;CAEA,OAAO;AACT;AAQA,SAAS,kBACP,QACA,OACA,OACA,QACA,gBACkB;CAClB,IAAI,QAA0B;CAE9B,UAAU,QAAQ,OAAO;EADP,OAAO,iBAAiB,QAAQ,OAAO,OAAO,MAAM;EAAG,KAAK,MAAM;CAC3D,GAAS,iBAAiB,OAAO,KAAK,aAAa;EAC1E,IAAI,QAAQ,QAAQ,OAAO;EAE3B,QAAQ;GAAE;GAAO;GAAK,SAAS;EAAS;EACxC,OAAO;CACT,CAAC;CAED,OAAO;AACT;AAKA,SAAS,iBACP,QACA,OACA,OACA,QACQ;CACR,IAAI,CAAC,gBAAgB,MAAM,IAAI,GAAG,OAAO,MAAM;CAE/C,MAAM,iBAAiB,OAAO;CAC9B,MAAM,OAAO,eAAe,eAAe,KAAK,IAAI,MAAM,OAAO,MAAM,CAAC;CACxE,OAAO,KAAK,IAAI,MAAM,OAAO,eAAe,GAAG,IAAI,KAAK,CAAC;AAC3D;AAEA,SAAS,gBAAgB,MAA8B;CACrD,OAAO,KAAK,SAAS,WAAW,KAAK,SAAS;AAChD;AAEA,SAAS,gBACP,QACA,OACA,QACA,QACA,SACkB;CAClB,KAAK,MAAM,SAAS,OAAO,WAAW,GAAG;EACvC,IAAI,MAAM,QAAQ,QAAQ;EAE1B,MAAM,QAAQ,iBAAiB,QAAQ,OAAO,OAAO,QAAQ,OAAO;EACpE,IAAI,OAAO,OAAO;CACpB;CAEA,OAAO;AACT;AAKA,SAAS,iBACP,QACA,OACA,UACA,QACA,SACkB;CAIlB,IAAI,CAAC,gBAAgB,MAAM,IAAI,GAC7B,OAAO,kBAAkB,QAAQ,OAAO,UAAU,QAAQ,OAAO;CAEnE,MAAM,QAAQ;EAAE,OAAO,SAAS;EAAO,KAAK,KAAK,IAAI,SAAS,KAAK,MAAM;CAAE;CAC3E,MAAM,iBAAiB,OAAO;CAC9B,IAAI,OAAO,eAAe,eAAe,MAAM,GAAG;CAClD,OAAO,QAAQ,GAAG;EAChB,MAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,eAAe,GAAG,IAAI,KAAK,KAAK,iBAAiB;EACtF,MAAM,QAAQ,eAAe,eAAe,KAAK;EACjD,MAAM,aAAa,eAAe,GAAG,KAAK,KAAK;EAC/C,MAAM,SAAS;GACb,OAAO,KAAK,IAAI,MAAM,OAAO,UAAU;GACvC,KAAK,KAAK,IAAI,MAAM,KAAK,UAAU,QAAQ,IAAI,CAAC;EAClD;EACA,MAAM,QACJ,OAAO,MAAM,OAAO,QAAQ,OAAO,kBAAkB,QAAQ,OAAO,QAAQ,QAAQ,OAAO;EAC7F,IAAI,OAAO,OAAO;EAClB,IAAI,cAAc,MAAM,OAAO,OAAO;EAEtC,OAAO,QAAQ;CACjB;CAEA,OAAO;AACT;AAOA,SAAS,kBACP,QACA,OACA,OACA,QACA,SACkB;CAClB,IAAI,OAAyB;CAC7B,IAAI,aAA+B;CACnC,UAAU,QAAQ,OAAO,OAAO,QAAQ,kBAAkB,QAAQ,OAAO,KAAK,aAAa;EACzF,IAAI,MAAM,QAAQ,OAAO;EAEzB,aAAa;EACb,OAAO;GAAE;GAAO;GAAK,SAAS;EAAS;EACvC,OAAO;CACT,CAAC;CACD,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,CAAC,kBAAkB,MAAM,QAAQ,QAAQ,4BAA4B,KAAK,GAAG,OAAO;CAExF,OAAO;AACT;AAIA,SAAS,oBAAoB,QAAwB,QAAwB;CAC3E,MAAM,OAAO,OAAO,UAAU,QAAQ,KAAK,IAAI,OAAO,QAAQ,SAAS,CAAC,CAAC;CACzE,OAAO,SAAS,KAAK,IAAI,GAAG,gBAAgB,MAAM,CAAC,CAAC;AACtD;AAMA,SAAS,8BAA8B,cAA+B;CACpE,OAAO,aAAa,YAAY,MAAM,aAAa,YAAY;AACjE;;;;;;;;;;;AAYA,SAAS,kBAAkB,SAA0B;CACnD,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,OAAO,QAAQ;EACrB,IAAI,SAAS,MAAM,OAAO;EAC1B,IAAI,SAAS,KAAK;GAChB,MAAM,MAAM,kBAAkB,SAAS,KAAK;GAC5C,IAAI,QAAQ,QAAQ,CAAC,yBAAyB,QAAQ,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,OAAO;GAErF,QAAQ;GACR;EACF;EAEA,IAAI,SAAS,OAAO,CAAC,sBAAsB,SAAS,KAAK,GAAG,OAAO;EACnE,IAAI,SAAS,MAAM;EAEnB,SAAS;EACT,MAAM,UAAU,QAAQ;EACxB,IAAI,YAAY,KAAA,GAAW,OAAO;EAGlC,IAAI,CAAC,kBAAkB,IAAI,OAAO,KAAK,gBAAgB,KAAK,OAAO,GAAG,OAAO;CAC/E;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAS,sBAAsB,SAAiB,OAAwB;CACtE,IAAI,QAAQ,QAAQ,OAAO,KAAK,OAAO;CAEvC,MAAM,OAAO,QAAQ,QAAQ;CAE7B,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS;AAClE;AAIA,SAAS,yBAAyB,MAAuB;CACvD,IAAI,KAAK,WAAW,GAAG,GAAG,OAAO;CACjC,OAAO,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,SAAS,IAAI;AACpD;AAEA,SAAS,kBAAkB,SAAiB,MAA6B;CACvE,KAAK,IAAI,QAAQ,OAAO,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAC1D,IAAI,QAAQ,WAAW,MAAM,SAAS;MACjC,IAAI,QAAQ,WAAW,KAAK,OAAO;CAG1C,OAAO;AACT;AAEA,SAAS,iBAAiB,OAA4C;CACpE,IAAI,MAAM,aAAa,WAAW,GAAG,OAAO;CAE5C,MAAM,SAAS,MAAM,UAAU,MAAM,eAAe,uBAAuB,MAAM,YAAY;CAC7F,MAAM,OAAO,cAAc,KAAK;CAChC,MAAM,eAAe,KAAK,SAAS,WAAW,KAAK,SAAS;CAI5D,MAAM,QAAQ,IAAI,MAAM,YAAY,KAAK,MAAM,eAAe,MAAM;CAOpE,MAAM,qBACJ,CAAC,MAAM,YAAY,MAAM,aAAa,CAAC,8BAA8B,MAAM,YAAY;CAEzF,MAAM,QAAQ,aAAa,QAAQ,KAAK;CACxC,IAAI,CAAC,OAAO,OAAO;CAEnB,OAAO;EACL;EACA,cAAc,qBAAqB,MAAM,eAAe;EACxD,WAAW,MAAM;EACjB;CACF;AACF;AAEA,SAAS,cAAc,OAAiC;CACtD,IAAI,CAAC,MAAM,SAAS;EAClB,MAAM,aAAa,MAAM,aAAa,MAAM,IAAI,CAAC,CAAC,SAAS;EAC3D,OAAO,eAAe,IAAI,EAAE,MAAM,QAAQ,IAAI;GAAE,MAAM;GAAiB;EAAW;CACpF;CAEA,IAAI,CAAC,kBAAkB,MAAM,YAAY,GACvC,OAAO;EAAE,MAAM;EAAS,QAAQ;CAA+B;CACjE,OAAO,cAAc,MAAM,YAAY,IAAI,EAAE,MAAM,iBAAiB,IAAI,EAAE,MAAM,QAAQ;AAC1F;AAIA,SAAS,cAAc,SAA0B;CAC/C,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,OAAO,QAAQ;EACrB,IAAI,SAAS,MAAM,SAAS;OACvB,IAAI,SAAS,KAAK,QAAQ,kBAAkB,SAAS,KAAK,KAAK,QAAQ;OACvE,IAAI,SAAS,OAAO,SAAS,KAAK,OAAO;CAChD;CAEA,OAAO;AACT;;;;;;;;;;;;;AAcA,SAAS,aAAa,QAAgB,OAA8B;CAClE,IAAI;EACF,OAAO,IAAI,OAAO,QAAQ,GAAG,MAAM,EAAE;CACvC,QAAQ,CAGR;CAEA,IAAI;EACF,OAAO,IAAI,OAAO,QAAQ,KAAK;CACjC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,aACP,QACA,QACsB;CACtB,IAAI,CAAC,UAAU,OAAO,WAAW,GAAG,OAAO,CAAC;EAAE,OAAO;EAAG,KAAK,OAAO;CAAO,CAAC;CAE5E,MAAM,UAAU,OACb,KAAK,WAAW;EACf,OAAO,kBAAkB,QAAQ,MAAM,KAAK;EAC5C,KAAK,kBAAkB,QAAQ,MAAM,GAAG;CAC1C,EAAE,CAAC,CACF,QAAQ,UAAU,MAAM,SAAS,MAAM,GAAG,CAAC,CAC3C,SAAS,uBAAuB;CAKnC,MAAM,SAAsB,CAAC;CAC7B,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,OAAO,GAAG,EAAE;EAC7B,IAAI,YAAY,MAAM,SAAS,SAAS,KAAK;GAC3C,OAAO,OAAO,SAAS,KAAK;IAAE,OAAO,SAAS;IAAO,KAAK,KAAK,IAAI,SAAS,KAAK,MAAM,GAAG;GAAE;GAC5F;EACF;EAEA,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO;AACT;AAEA,SAAS,kBAAkB,QAAwB,QAAwB;CACzE,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO,MAAM;AACpD;;AAMA,SAAS,UACP,QACA,OACA,OACA,gBACA,OACS;CACT,IAAI,MAAM,KAAK,SAAS,SACtB,OAAO,eAAe,QAAQ,OAAO,OAAO,gBAAgB,KAAK;CACnE,IAAI,MAAM,KAAK,SAAS,iBACtB,OAAO,oBAAoB,QAAQ,OAAO,OAAO,MAAM,KAAK,YAAY,KAAK;CAE/E,OAAO,UAAU,QAAQ,OAAO,OAAO,gBAAgB,KAAK;AAC9D;AASA,SAAS,UACP,QACA,OACA,OACA,gBACA,OACS;CACT,MAAM,iBAAiB,OAAO;CAC9B,IAAI,QAAQ,eAAe,eAAe,MAAM,KAAK;CACrD,OAAO,QAAQ,eAAe,QAAQ;EACpC,MAAM,QAAQ,eAAe,GAAG,KAAK;EACrC,IAAI,UAAU,KAAA,KAAa,QAAQ,MAAM,KAAK,OAAO;EAErD,MAAM,OAAO,eAAe,QAAQ,OAAO,OAAO,MAAM,GAAG;EAE3D,IAAI,CAAC,eADU,WAAW,QAAQ,OAAO,UAAU,QAAQ,IAAI,GAAG,KAC9C,GAAQ,OAAO,OAAO,gBAAgB,KAAK,GAAG,OAAO;EACzE,QAAQ,OAAO;CACjB;CAEA,OAAO;AACT;AAEA,SAAS,eACP,QACA,OACA,OACA,UACQ;CACR,MAAM,QAAQ,KAAK,IAAI,UAAU,QAAQ,iBAAiB;CAC1D,OAAO,KAAK,IAAI,OAAO,OAAO,eAAe,eAAe,KAAK,CAAC;AACpE;AAEA,SAAS,eACP,QACA,OACA,OACA,gBACA,OACS;CACT,IAAI,MAAM,KAAK,SAAS,kBACtB,OAAO,WAAW,QAAQ,OAAO,OAAO,gBAAgB,KAAK;CAG/D,IAAI,YAAY;CAChB,OAAO,aAAa,OAAO,KAAK,QAAQ;EACtC,MAAM,QAAQ,OAAO,KAAK,QAAQ,MAAM,SAAS;EACjD,MAAM,UAAU,UAAU,KAAK,OAAO,KAAK,SAAS;EACpD,MAAM,OAAO;GACX,MAAM,OAAO,KAAK,MAAM,WAAW,OAAO;GAC1C,OAAO,OAAO,QAAQ;GACtB,MAAM,KAAK,IAAI,GAAG,OAAO,OAAO,SAAS;EAC3C;EACA,IAAI,WAAW,OAAO,QAAQ,CAAC,WAAW,MAAM,OAAO,OAAO,gBAAgB,KAAK,GACjF,OAAO;EAET,YAAY,UAAU;CACxB;CAEA,OAAO;AACT;AAOA,SAAS,oBACP,QACA,OACA,OACA,YACA,OACS;CACT,MAAM,iBAAiB,OAAO;CAC9B,MAAM,YAAY,eAAe,eAAe,MAAM,GAAG;CACzD,IAAI,QAAQ,eAAe,eAAe,MAAM,KAAK;CACrD,MAAM,SAAS,EAAE,QAAQ,MAAM,MAAM;CACrC,OAAO,SAAS,WAAW;EACzB,MAAM,QAAQ,eAAe,GAAG,KAAK,KAAK;EAC1C,MAAM,OAAO,KAAK,IAChB,WACA,KAAK,IAAI,QAAQ,YAAY,eAAe,QAAQ,OAAO,OAAO,MAAM,GAAG,CAAC,CAC9E;EAGA,IAAI,CAAC,WAAW;GADC,MADJ,OAAO,UAAU,OAAO,KAAK,IAAI,OAAO,UAAU,QAAQ,IAAI,CAAC,CAC3D;GAAM;GAAO,MAAM,KAAK,IAAI,GAAG,OAAO,SAAS,KAAK;EACrD,GAAQ,OAAO,OAAO,OAAO,OAAO,MAAM,GAAG,OAAO;EACpE,IAAI,SAAS,WAAW,OAAO;EAE/B,QAAQ,OAAO,IAAI;CACrB;CAEA,OAAO;AACT;AAOA,SAAS,eACP,QACA,OACA,OACA,gBACA,OACS;CACT,MAAM,iBAAiB,OAAO;CAC9B,MAAM,QAAQ,eAAe,GAAG,eAAe,eAAe,MAAM,KAAK,CAAC,KAAK;CAC/E,MAAM,MAAM,UAAU,QAAQ,eAAe,eAAe,MAAM,GAAG,CAAC;CAEtE,OAAO,WADQ,WAAW,QAAQ,OAAO,KAAK,IAAI,OAAO,GAAG,GAAG,KAC7C,GAAQ,OAAO,OAAO,gBAAgB,KAAK;AAC/D;AAEA,SAAS,WACP,QACA,OACA,KACA,OACY;CACZ,OAAO;EAAE,MAAM,OAAO,UAAU,OAAO,GAAG;EAAG;EAAO,MAAM,KAAK,IAAI,GAAG,MAAM,QAAQ,KAAK;CAAE;AAC7F;;AAOA,SAAS,WACP,QACA,OACA,OACA,gBACA,OACA,QACS;CACT,IAAI,MAAM,gBAAgB,CAAC,gBACzB,OAAO,kBAAkB,QAAQ,MAAM,cAAc,MAAM,WAAW,OAAO,OAAO,MAAM;CAE5F,OAAO,iBAAiB,QAAQ,OAAO,OAAO,gBAAgB,OAAO,MAAM;AAC7E;AAEA,SAAS,kBACP,QACA,cACA,WACA,OACA,OACA,QACS;CAGT,IAAI,QAAQ,OAAO,KAAK,QAAQ,cAAc,OAAO,IAAI;CACzD,OAAO,UAAU,IAAI;EACnB,MAAM,QAAQ,OAAO,QAAQ;EAC7B,MAAM,MAAM,QAAQ,aAAa;EACjC,IAAI,MAAM,MAAM,KAAK,OAAO;EAC5B,IAAI,QAAQ,OAAO,SAAS;EAC5B,IAAI,oBAAoB,OAAO,MAAM,OAAO,aAAa,QAAQ,SAAS,GACpE;OAAA,CAAC,MAAM,OAAO,KAAK,IAAI,GAAG,OAAO;EAAA;EAGvC,QAAQ,OAAO,KAAK,QAAQ,cAAc,QAAQ,aAAa,MAAM;CACvE;CAEA,OAAO;AACT;AAEA,SAAS,iBACP,QACA,OACA,OACA,gBACA,OACA,QACS;CACT,MAAM,MAAM,YAAY,OAAO;CAC/B,OAAO,MAAM;EACX,MAAM,QAAQ,MAAM,MAAM,KAAK,OAAO,IAAI;EAC1C,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,QAAQ,OAAO,QAAQ,MAAM;EACnC,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC;EAC7B,IAAI,QAAQ,MAAM,OAAO,MAAM,MAAM,KAAK,OAAO;EACjD,IAAI,QAAQ,OAAO,SAAS;EAC5B,IAAI,oBAAoB,OAAO,MAAM,MAAM,OAAO,MAAM,EAAE,CAAC,QAAQ,MAAM,SAAS,GAC5E;OAAA,CAAC,MAAM,OAAO,KAAK,iBAAiB,QAAQ,IAAI,GAAG,OAAO;EAAA;EAGhE,IAAI,MAAM,EAAE,CAAC,WAAW,GAAG,sBAAsB,MAAM,OAAO,OAAO,IAAI;CAC3E;AACF;AAEA,SAAS,sBAAsB,OAAe,MAAoB;CAChE,MAAM,UAAU,MAAM;CACtB,IAAI,UAAU,KAAK,QAAQ;CAE3B,MAAM,OAAO,gBAAgB,MAAM,OAAO;CAC1C,MAAM,YAAY,UAAU,KAAK,IAAI,GAAG,IAAI;AAC9C;AAEA,SAAS,oBACP,MACA,OACA,QACA,WACS;CACT,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO,iBAAiB,MAAM;EAAE;EAAO,KAAK,QAAQ;CAAO,CAAC;AAC9D"}
|