1 | import { EditorView, Decoration, ViewPlugin, runScopeHandlers } from '@codemirror/view';
|
2 | import { StateEffect, StateField, EditorSelection, Facet, combineConfig, CharCategory, Prec } from '@codemirror/state';
|
3 | import { showPanel, getPanel } from '@codemirror/panel';
|
4 | import { RangeSetBuilder } from '@codemirror/rangeset';
|
5 | import elt from 'crelt';
|
6 | import { codePointAt, fromCodePoint, codePointSize } from '@codemirror/text';
|
7 |
|
8 | const basicNormalize = typeof String.prototype.normalize == "function"
|
9 | ? x => x.normalize("NFKD") : x => x;
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | class SearchCursor {
|
15 | |
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | constructor(text, query, from = 0, to = text.length, normalize) {
|
29 | |
30 |
|
31 |
|
32 |
|
33 |
|
34 | this.value = { from: 0, to: 0 };
|
35 | |
36 |
|
37 |
|
38 | this.done = false;
|
39 | this.matches = [];
|
40 | this.buffer = "";
|
41 | this.bufferPos = 0;
|
42 | this.iter = text.iterRange(from, to);
|
43 | this.bufferStart = from;
|
44 | this.normalize = normalize ? x => normalize(basicNormalize(x)) : basicNormalize;
|
45 | this.query = this.normalize(query);
|
46 | }
|
47 | peek() {
|
48 | if (this.bufferPos == this.buffer.length) {
|
49 | this.bufferStart += this.buffer.length;
|
50 | this.iter.next();
|
51 | if (this.iter.done)
|
52 | return -1;
|
53 | this.bufferPos = 0;
|
54 | this.buffer = this.iter.value;
|
55 | }
|
56 | return codePointAt(this.buffer, this.bufferPos);
|
57 | }
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | next() {
|
65 | while (this.matches.length)
|
66 | this.matches.pop();
|
67 | return this.nextOverlapping();
|
68 | }
|
69 | |
70 |
|
71 |
|
72 |
|
73 |
|
74 | nextOverlapping() {
|
75 | for (;;) {
|
76 | let next = this.peek();
|
77 | if (next < 0) {
|
78 | this.done = true;
|
79 | return this;
|
80 | }
|
81 | let str = fromCodePoint(next), start = this.bufferStart + this.bufferPos;
|
82 | this.bufferPos += codePointSize(next);
|
83 | let norm = this.normalize(str);
|
84 | for (let i = 0, pos = start;; i++) {
|
85 | let code = norm.charCodeAt(i);
|
86 | let match = this.match(code, pos);
|
87 | if (match) {
|
88 | this.value = match;
|
89 | return this;
|
90 | }
|
91 | if (i == norm.length - 1)
|
92 | break;
|
93 | if (pos == start && i < str.length && str.charCodeAt(i) == code)
|
94 | pos++;
|
95 | }
|
96 | }
|
97 | }
|
98 | match(code, pos) {
|
99 | let match = null;
|
100 | for (let i = 0; i < this.matches.length; i += 2) {
|
101 | let index = this.matches[i], keep = false;
|
102 | if (this.query.charCodeAt(index) == code) {
|
103 | if (index == this.query.length - 1) {
|
104 | match = { from: this.matches[i + 1], to: pos + 1 };
|
105 | }
|
106 | else {
|
107 | this.matches[i]++;
|
108 | keep = true;
|
109 | }
|
110 | }
|
111 | if (!keep) {
|
112 | this.matches.splice(i, 2);
|
113 | i -= 2;
|
114 | }
|
115 | }
|
116 | if (this.query.charCodeAt(0) == code) {
|
117 | if (this.query.length == 1)
|
118 | match = { from: pos, to: pos + 1 };
|
119 | else
|
120 | this.matches.push(1, pos);
|
121 | }
|
122 | return match;
|
123 | }
|
124 | }
|
125 | if (typeof Symbol != "undefined")
|
126 | SearchCursor.prototype[Symbol.iterator] = function () { return this; };
|
127 |
|
128 | const empty = { from: -1, to: -1, match: /.*/.exec("") };
|
129 | const baseFlags = "gm" + (/x/.unicode == null ? "" : "u");
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | class RegExpCursor {
|
136 | |
137 |
|
138 |
|
139 |
|
140 |
|
141 | constructor(text, query, options, from = 0, to = text.length) {
|
142 | this.to = to;
|
143 | this.curLine = "";
|
144 | |
145 |
|
146 |
|
147 |
|
148 | this.done = false;
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 | this.value = empty;
|
155 | if (/\\[sWDnr]|\n|\r|\[\^/.test(query))
|
156 | return new MultilineRegExpCursor(text, query, options, from, to);
|
157 | this.re = new RegExp(query, baseFlags + ((options === null || options === void 0 ? void 0 : options.ignoreCase) ? "i" : ""));
|
158 | this.iter = text.iter();
|
159 | let startLine = text.lineAt(from);
|
160 | this.curLineStart = startLine.from;
|
161 | this.matchPos = from;
|
162 | this.getLine(this.curLineStart);
|
163 | }
|
164 | getLine(skip) {
|
165 | this.iter.next(skip);
|
166 | if (this.iter.lineBreak) {
|
167 | this.curLine = "";
|
168 | }
|
169 | else {
|
170 | this.curLine = this.iter.value;
|
171 | if (this.curLineStart + this.curLine.length > this.to)
|
172 | this.curLine = this.curLine.slice(0, this.to - this.curLineStart);
|
173 | this.iter.next();
|
174 | }
|
175 | }
|
176 | nextLine() {
|
177 | this.curLineStart = this.curLineStart + this.curLine.length + 1;
|
178 | if (this.curLineStart > this.to)
|
179 | this.curLine = "";
|
180 | else
|
181 | this.getLine(0);
|
182 | }
|
183 | |
184 |
|
185 |
|
186 | next() {
|
187 | for (let off = this.matchPos - this.curLineStart;;) {
|
188 | this.re.lastIndex = off;
|
189 | let match = this.matchPos <= this.to && this.re.exec(this.curLine);
|
190 | if (match) {
|
191 | let from = this.curLineStart + match.index, to = from + match[0].length;
|
192 | this.matchPos = to + (from == to ? 1 : 0);
|
193 | if (from == this.curLine.length)
|
194 | this.nextLine();
|
195 | if (from < to || from > this.value.to) {
|
196 | this.value = { from, to, match };
|
197 | return this;
|
198 | }
|
199 | off = this.matchPos - this.curLineStart;
|
200 | }
|
201 | else if (this.curLineStart + this.curLine.length < this.to) {
|
202 | this.nextLine();
|
203 | off = 0;
|
204 | }
|
205 | else {
|
206 | this.done = true;
|
207 | return this;
|
208 | }
|
209 | }
|
210 | }
|
211 | }
|
212 | const flattened = new WeakMap();
|
213 |
|
214 | class FlattenedDoc {
|
215 | constructor(from, text) {
|
216 | this.from = from;
|
217 | this.text = text;
|
218 | }
|
219 | get to() { return this.from + this.text.length; }
|
220 | static get(doc, from, to) {
|
221 | let cached = flattened.get(doc);
|
222 | if (!cached || cached.from >= to || cached.to <= from) {
|
223 | let flat = new FlattenedDoc(from, doc.sliceString(from, to));
|
224 | flattened.set(doc, flat);
|
225 | return flat;
|
226 | }
|
227 | if (cached.from == from && cached.to == to)
|
228 | return cached;
|
229 | let { text, from: cachedFrom } = cached;
|
230 | if (cachedFrom > from) {
|
231 | text = doc.sliceString(from, cachedFrom) + text;
|
232 | cachedFrom = from;
|
233 | }
|
234 | if (cached.to < to)
|
235 | text += doc.sliceString(cached.to, to);
|
236 | flattened.set(doc, new FlattenedDoc(cachedFrom, text));
|
237 | return new FlattenedDoc(from, text.slice(from - cachedFrom, to - cachedFrom));
|
238 | }
|
239 | }
|
240 | class MultilineRegExpCursor {
|
241 | constructor(text, query, options, from, to) {
|
242 | this.text = text;
|
243 | this.to = to;
|
244 | this.done = false;
|
245 | this.value = empty;
|
246 | this.matchPos = from;
|
247 | this.re = new RegExp(query, baseFlags + ((options === null || options === void 0 ? void 0 : options.ignoreCase) ? "i" : ""));
|
248 | this.flat = FlattenedDoc.get(text, from, this.chunkEnd(from + 5000 ));
|
249 | }
|
250 | chunkEnd(pos) {
|
251 | return pos >= this.to ? this.to : this.text.lineAt(pos).to;
|
252 | }
|
253 | next() {
|
254 | for (;;) {
|
255 | let off = this.re.lastIndex = this.matchPos - this.flat.from;
|
256 | let match = this.re.exec(this.flat.text);
|
257 |
|
258 | if (match && !match[0] && match.index == off) {
|
259 | this.re.lastIndex = off + 1;
|
260 | match = this.re.exec(this.flat.text);
|
261 | }
|
262 |
|
263 |
|
264 | if (match && this.flat.to < this.to && match.index + match[0].length > this.flat.text.length - 10)
|
265 | match = null;
|
266 | if (match) {
|
267 | let from = this.flat.from + match.index, to = from + match[0].length;
|
268 | this.value = { from, to, match };
|
269 | this.matchPos = to + (from == to ? 1 : 0);
|
270 | return this;
|
271 | }
|
272 | else {
|
273 | if (this.flat.to == this.to) {
|
274 | this.done = true;
|
275 | return this;
|
276 | }
|
277 |
|
278 | this.flat = FlattenedDoc.get(this.text, this.flat.from, this.chunkEnd(this.flat.from + this.flat.text.length * 2));
|
279 | }
|
280 | }
|
281 | }
|
282 | }
|
283 | if (typeof Symbol != "undefined") {
|
284 | RegExpCursor.prototype[Symbol.iterator] = MultilineRegExpCursor.prototype[Symbol.iterator] =
|
285 | function () { return this; };
|
286 | }
|
287 | function validRegExp(source) {
|
288 | try {
|
289 | new RegExp(source, baseFlags);
|
290 | return true;
|
291 | }
|
292 | catch (_a) {
|
293 | return false;
|
294 | }
|
295 | }
|
296 |
|
297 | function createLineDialog(view) {
|
298 | let input = elt("input", { class: "cm-textfield", name: "line" });
|
299 | let dom = elt("form", {
|
300 | class: "cm-gotoLine",
|
301 | onkeydown: (event) => {
|
302 | if (event.keyCode == 27) {
|
303 | event.preventDefault();
|
304 | view.dispatch({ effects: dialogEffect.of(false) });
|
305 | view.focus();
|
306 | }
|
307 | else if (event.keyCode == 13) {
|
308 | event.preventDefault();
|
309 | go();
|
310 | }
|
311 | },
|
312 | onsubmit: (event) => {
|
313 | event.preventDefault();
|
314 | go();
|
315 | }
|
316 | }, elt("label", view.state.phrase("Go to line"), ": ", input), " ", elt("button", { class: "cm-button", type: "submit" }, view.state.phrase("go")));
|
317 | function go() {
|
318 | let match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(input.value);
|
319 | if (!match)
|
320 | return;
|
321 | let { state } = view, startLine = state.doc.lineAt(state.selection.main.head);
|
322 | let [, sign, ln, cl, percent] = match;
|
323 | let col = cl ? +cl.slice(1) : 0;
|
324 | let line = ln ? +ln : startLine.number;
|
325 | if (ln && percent) {
|
326 | let pc = line / 100;
|
327 | if (sign)
|
328 | pc = pc * (sign == "-" ? -1 : 1) + (startLine.number / state.doc.lines);
|
329 | line = Math.round(state.doc.lines * pc);
|
330 | }
|
331 | else if (ln && sign) {
|
332 | line = line * (sign == "-" ? -1 : 1) + startLine.number;
|
333 | }
|
334 | let docLine = state.doc.line(Math.max(1, Math.min(state.doc.lines, line)));
|
335 | view.dispatch({
|
336 | effects: dialogEffect.of(false),
|
337 | selection: EditorSelection.cursor(docLine.from + Math.max(0, Math.min(col, docLine.length))),
|
338 | scrollIntoView: true
|
339 | });
|
340 | view.focus();
|
341 | }
|
342 | return { dom, pos: -10 };
|
343 | }
|
344 | const dialogEffect = StateEffect.define();
|
345 | const dialogField = StateField.define({
|
346 | create() { return true; },
|
347 | update(value, tr) {
|
348 | for (let e of tr.effects)
|
349 | if (e.is(dialogEffect))
|
350 | value = e.value;
|
351 | return value;
|
352 | },
|
353 | provide: f => showPanel.from(f, val => val ? createLineDialog : null)
|
354 | });
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 | const gotoLine = view => {
|
368 | let panel = getPanel(view, createLineDialog);
|
369 | if (!panel) {
|
370 | let effects = [dialogEffect.of(true)];
|
371 | if (view.state.field(dialogField, false) == null)
|
372 | effects.push(StateEffect.appendConfig.of([dialogField, baseTheme$1]));
|
373 | view.dispatch({ effects });
|
374 | panel = getPanel(view, createLineDialog);
|
375 | }
|
376 | if (panel)
|
377 | panel.dom.querySelector("input").focus();
|
378 | return true;
|
379 | };
|
380 | const baseTheme$1 = EditorView.baseTheme({
|
381 | ".cm-panel.cm-gotoLine": {
|
382 | padding: "2px 6px 4px",
|
383 | "& label": { fontSize: "80%" }
|
384 | }
|
385 | });
|
386 |
|
387 | const defaultHighlightOptions = {
|
388 | highlightWordAroundCursor: false,
|
389 | minSelectionLength: 1,
|
390 | maxMatches: 100
|
391 | };
|
392 | const highlightConfig = Facet.define({
|
393 | combine(options) {
|
394 | return combineConfig(options, defaultHighlightOptions, {
|
395 | highlightWordAroundCursor: (a, b) => a || b,
|
396 | minSelectionLength: Math.min,
|
397 | maxMatches: Math.min
|
398 | });
|
399 | }
|
400 | });
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | function highlightSelectionMatches(options) {
|
408 | let ext = [defaultTheme, matchHighlighter];
|
409 | if (options)
|
410 | ext.push(highlightConfig.of(options));
|
411 | return ext;
|
412 | }
|
413 | const matchDeco = Decoration.mark({ class: "cm-selectionMatch" });
|
414 | const mainMatchDeco = Decoration.mark({ class: "cm-selectionMatch cm-selectionMatch-main" });
|
415 | const matchHighlighter = ViewPlugin.fromClass(class {
|
416 | constructor(view) {
|
417 | this.decorations = this.getDeco(view);
|
418 | }
|
419 | update(update) {
|
420 | if (update.selectionSet || update.docChanged || update.viewportChanged)
|
421 | this.decorations = this.getDeco(update.view);
|
422 | }
|
423 | getDeco(view) {
|
424 | let conf = view.state.facet(highlightConfig);
|
425 | let { state } = view, sel = state.selection;
|
426 | if (sel.ranges.length > 1)
|
427 | return Decoration.none;
|
428 | let range = sel.main, query, check = null;
|
429 | if (range.empty) {
|
430 | if (!conf.highlightWordAroundCursor)
|
431 | return Decoration.none;
|
432 | let word = state.wordAt(range.head);
|
433 | if (!word)
|
434 | return Decoration.none;
|
435 | check = state.charCategorizer(range.head);
|
436 | query = state.sliceDoc(word.from, word.to);
|
437 | }
|
438 | else {
|
439 | let len = range.to - range.from;
|
440 | if (len < conf.minSelectionLength || len > 200)
|
441 | return Decoration.none;
|
442 | query = state.sliceDoc(range.from, range.to).trim();
|
443 | if (!query)
|
444 | return Decoration.none;
|
445 | }
|
446 | let deco = [];
|
447 | for (let part of view.visibleRanges) {
|
448 | let cursor = new SearchCursor(state.doc, query, part.from, part.to);
|
449 | while (!cursor.next().done) {
|
450 | let { from, to } = cursor.value;
|
451 | if (!check || ((from == 0 || check(state.sliceDoc(from - 1, from)) != CharCategory.Word) &&
|
452 | (to == state.doc.length || check(state.sliceDoc(to, to + 1)) != CharCategory.Word))) {
|
453 | if (check && from <= range.from && to >= range.to)
|
454 | deco.push(mainMatchDeco.range(from, to));
|
455 | else if (from >= range.to || to <= range.from)
|
456 | deco.push(matchDeco.range(from, to));
|
457 | if (deco.length > conf.maxMatches)
|
458 | return Decoration.none;
|
459 | }
|
460 | }
|
461 | }
|
462 | return Decoration.set(deco);
|
463 | }
|
464 | }, {
|
465 | decorations: v => v.decorations
|
466 | });
|
467 | const defaultTheme = EditorView.baseTheme({
|
468 | ".cm-selectionMatch": { backgroundColor: "#99ff7780" },
|
469 | ".cm-searchMatch .cm-selectionMatch": { backgroundColor: "transparent" }
|
470 | });
|
471 |
|
472 | const selectWord = ({ state, dispatch }) => {
|
473 | let { selection } = state;
|
474 | let newSel = EditorSelection.create(selection.ranges.map(range => state.wordAt(range.head) || EditorSelection.cursor(range.head)), selection.mainIndex);
|
475 | if (newSel.eq(selection))
|
476 | return false;
|
477 | dispatch(state.update({ selection: newSel }));
|
478 | return true;
|
479 | };
|
480 |
|
481 |
|
482 | function findNextOccurrence(state, query) {
|
483 | let { main, ranges } = state.selection;
|
484 | let word = state.wordAt(main.head), fullWord = word && word.from == main.from && word.to == main.to;
|
485 | for (let cycled = false, cursor = new SearchCursor(state.doc, query, ranges[ranges.length - 1].to);;) {
|
486 | cursor.next();
|
487 | if (cursor.done) {
|
488 | if (cycled)
|
489 | return null;
|
490 | cursor = new SearchCursor(state.doc, query, 0, Math.max(0, ranges[ranges.length - 1].from - 1));
|
491 | cycled = true;
|
492 | }
|
493 | else {
|
494 | if (cycled && ranges.some(r => r.from == cursor.value.from))
|
495 | continue;
|
496 | if (fullWord) {
|
497 | let word = state.wordAt(cursor.value.from);
|
498 | if (!word || word.from != cursor.value.from || word.to != cursor.value.to)
|
499 | continue;
|
500 | }
|
501 | return cursor.value;
|
502 | }
|
503 | }
|
504 | }
|
505 |
|
506 |
|
507 |
|
508 |
|
509 | const selectNextOccurrence = ({ state, dispatch }) => {
|
510 | let { ranges } = state.selection;
|
511 | if (ranges.some(sel => sel.from === sel.to))
|
512 | return selectWord({ state, dispatch });
|
513 | let searchedText = state.sliceDoc(ranges[0].from, ranges[0].to);
|
514 | if (state.selection.ranges.some(r => state.sliceDoc(r.from, r.to) != searchedText))
|
515 | return false;
|
516 | let range = findNextOccurrence(state, searchedText);
|
517 | if (!range)
|
518 | return false;
|
519 | dispatch(state.update({
|
520 | selection: state.selection.addRange(EditorSelection.range(range.from, range.to), false),
|
521 | effects: EditorView.scrollIntoView(range.to)
|
522 | }));
|
523 | return true;
|
524 | };
|
525 |
|
526 | const searchConfigFacet = Facet.define({
|
527 | combine(configs) {
|
528 | var _a;
|
529 | return {
|
530 | top: configs.reduce((val, conf) => val !== null && val !== void 0 ? val : conf.top, undefined) || false,
|
531 | caseSensitive: configs.reduce((val, conf) => val !== null && val !== void 0 ? val : (conf.caseSensitive || conf.matchCase), undefined) || false,
|
532 | createPanel: ((_a = configs.find(c => c.createPanel)) === null || _a === void 0 ? void 0 : _a.createPanel) || (view => new SearchPanel(view))
|
533 | };
|
534 | }
|
535 | });
|
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 | function search(config) {
|
543 | return config ? [searchConfigFacet.of(config), searchExtensions] : searchExtensions;
|
544 | }
|
545 |
|
546 |
|
547 |
|
548 | const searchConfig = search;
|
549 |
|
550 |
|
551 |
|
552 | class SearchQuery {
|
553 | |
554 |
|
555 |
|
556 | constructor(config) {
|
557 | this.search = config.search;
|
558 | this.caseSensitive = !!config.caseSensitive;
|
559 | this.regexp = !!config.regexp;
|
560 | this.replace = config.replace || "";
|
561 | this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
|
562 | }
|
563 | |
564 |
|
565 |
|
566 | eq(other) {
|
567 | return this.search == other.search && this.replace == other.replace &&
|
568 | this.caseSensitive == other.caseSensitive && this.regexp == other.regexp;
|
569 | }
|
570 | |
571 |
|
572 |
|
573 | create() {
|
574 | return this.regexp ? new RegExpQuery(this) : new StringQuery(this);
|
575 | }
|
576 | }
|
577 | class QueryType {
|
578 | constructor(spec) {
|
579 | this.spec = spec;
|
580 | }
|
581 | }
|
582 | class StringQuery extends QueryType {
|
583 | constructor(spec) {
|
584 | super(spec);
|
585 | this.unquoted = spec.search.replace(/\\([nrt\\])/g, (_, ch) => ch == "n" ? "\n" : ch == "r" ? "\r" : ch == "t" ? "\t" : "\\");
|
586 | }
|
587 | cursor(doc, from = 0, to = doc.length) {
|
588 | return new SearchCursor(doc, this.unquoted, from, to, this.spec.caseSensitive ? undefined : x => x.toLowerCase());
|
589 | }
|
590 | nextMatch(doc, curFrom, curTo) {
|
591 | let cursor = this.cursor(doc, curTo).nextOverlapping();
|
592 | if (cursor.done)
|
593 | cursor = this.cursor(doc, 0, curFrom).nextOverlapping();
|
594 | return cursor.done ? null : cursor.value;
|
595 | }
|
596 |
|
597 |
|
598 | prevMatchInRange(doc, from, to) {
|
599 | for (let pos = to;;) {
|
600 | let start = Math.max(from, pos - 10000 - this.unquoted.length);
|
601 | let cursor = this.cursor(doc, start, pos), range = null;
|
602 | while (!cursor.nextOverlapping().done)
|
603 | range = cursor.value;
|
604 | if (range)
|
605 | return range;
|
606 | if (start == from)
|
607 | return null;
|
608 | pos -= 10000 ;
|
609 | }
|
610 | }
|
611 | prevMatch(doc, curFrom, curTo) {
|
612 | return this.prevMatchInRange(doc, 0, curFrom) ||
|
613 | this.prevMatchInRange(doc, curTo, doc.length);
|
614 | }
|
615 | getReplacement(_result) { return this.spec.replace; }
|
616 | matchAll(doc, limit) {
|
617 | let cursor = this.cursor(doc), ranges = [];
|
618 | while (!cursor.next().done) {
|
619 | if (ranges.length >= limit)
|
620 | return null;
|
621 | ranges.push(cursor.value);
|
622 | }
|
623 | return ranges;
|
624 | }
|
625 | highlight(doc, from, to, add) {
|
626 | let cursor = this.cursor(doc, Math.max(0, from - this.unquoted.length), Math.min(to + this.unquoted.length, doc.length));
|
627 | while (!cursor.next().done)
|
628 | add(cursor.value.from, cursor.value.to);
|
629 | }
|
630 | }
|
631 | class RegExpQuery extends QueryType {
|
632 | cursor(doc, from = 0, to = doc.length) {
|
633 | return new RegExpCursor(doc, this.spec.search, this.spec.caseSensitive ? undefined : { ignoreCase: true }, from, to);
|
634 | }
|
635 | nextMatch(doc, curFrom, curTo) {
|
636 | let cursor = this.cursor(doc, curTo).next();
|
637 | if (cursor.done)
|
638 | cursor = this.cursor(doc, 0, curFrom).next();
|
639 | return cursor.done ? null : cursor.value;
|
640 | }
|
641 | prevMatchInRange(doc, from, to) {
|
642 | for (let size = 1;; size++) {
|
643 | let start = Math.max(from, to - size * 10000 );
|
644 | let cursor = this.cursor(doc, start, to), range = null;
|
645 | while (!cursor.next().done)
|
646 | range = cursor.value;
|
647 | if (range && (start == from || range.from > start + 10))
|
648 | return range;
|
649 | if (start == from)
|
650 | return null;
|
651 | }
|
652 | }
|
653 | prevMatch(doc, curFrom, curTo) {
|
654 | return this.prevMatchInRange(doc, 0, curFrom) ||
|
655 | this.prevMatchInRange(doc, curTo, doc.length);
|
656 | }
|
657 | getReplacement(result) {
|
658 | return this.spec.replace.replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
|
659 | : i == "&" ? result.match[0]
|
660 | : i != "0" && +i < result.match.length ? result.match[i]
|
661 | : m);
|
662 | }
|
663 | matchAll(doc, limit) {
|
664 | let cursor = this.cursor(doc), ranges = [];
|
665 | while (!cursor.next().done) {
|
666 | if (ranges.length >= limit)
|
667 | return null;
|
668 | ranges.push(cursor.value);
|
669 | }
|
670 | return ranges;
|
671 | }
|
672 | highlight(doc, from, to, add) {
|
673 | let cursor = this.cursor(doc, Math.max(0, from - 250 ), Math.min(to + 250 , doc.length));
|
674 | while (!cursor.next().done)
|
675 | add(cursor.value.from, cursor.value.to);
|
676 | }
|
677 | }
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 | const setSearchQuery = StateEffect.define();
|
686 | const togglePanel = StateEffect.define();
|
687 | const searchState = StateField.define({
|
688 | create(state) {
|
689 | return new SearchState(defaultQuery(state).create(), null);
|
690 | },
|
691 | update(value, tr) {
|
692 | for (let effect of tr.effects) {
|
693 | if (effect.is(setSearchQuery))
|
694 | value = new SearchState(effect.value.create(), value.panel);
|
695 | else if (effect.is(togglePanel))
|
696 | value = new SearchState(value.query, effect.value ? createSearchPanel : null);
|
697 | }
|
698 | return value;
|
699 | },
|
700 | provide: f => showPanel.from(f, val => val.panel)
|
701 | });
|
702 |
|
703 |
|
704 |
|
705 | function getSearchQuery(state) {
|
706 | let curState = state.field(searchState, false);
|
707 | return curState ? curState.query.spec : defaultQuery(state);
|
708 | }
|
709 | class SearchState {
|
710 | constructor(query, panel) {
|
711 | this.query = query;
|
712 | this.panel = panel;
|
713 | }
|
714 | }
|
715 | const matchMark = Decoration.mark({ class: "cm-searchMatch" }), selectedMatchMark = Decoration.mark({ class: "cm-searchMatch cm-searchMatch-selected" });
|
716 | const searchHighlighter = ViewPlugin.fromClass(class {
|
717 | constructor(view) {
|
718 | this.view = view;
|
719 | this.decorations = this.highlight(view.state.field(searchState));
|
720 | }
|
721 | update(update) {
|
722 | let state = update.state.field(searchState);
|
723 | if (state != update.startState.field(searchState) || update.docChanged || update.selectionSet)
|
724 | this.decorations = this.highlight(state);
|
725 | }
|
726 | highlight({ query, panel }) {
|
727 | if (!panel || !query.spec.valid)
|
728 | return Decoration.none;
|
729 | let { view } = this;
|
730 | let builder = new RangeSetBuilder();
|
731 | for (let i = 0, ranges = view.visibleRanges, l = ranges.length; i < l; i++) {
|
732 | let { from, to } = ranges[i];
|
733 | while (i < l - 1 && to > ranges[i + 1].from - 2 * 250 )
|
734 | to = ranges[++i].to;
|
735 | query.highlight(view.state.doc, from, to, (from, to) => {
|
736 | let selected = view.state.selection.ranges.some(r => r.from == from && r.to == to);
|
737 | builder.add(from, to, selected ? selectedMatchMark : matchMark);
|
738 | });
|
739 | }
|
740 | return builder.finish();
|
741 | }
|
742 | }, {
|
743 | decorations: v => v.decorations
|
744 | });
|
745 | function searchCommand(f) {
|
746 | return view => {
|
747 | let state = view.state.field(searchState, false);
|
748 | return state && state.query.spec.valid ? f(view, state) : openSearchPanel(view);
|
749 | };
|
750 | }
|
751 |
|
752 |
|
753 |
|
754 |
|
755 |
|
756 |
|
757 | const findNext = searchCommand((view, { query }) => {
|
758 | let { from, to } = view.state.selection.main;
|
759 | let next = query.nextMatch(view.state.doc, from, to);
|
760 | if (!next || next.from == from && next.to == to)
|
761 | return false;
|
762 | view.dispatch({
|
763 | selection: { anchor: next.from, head: next.to },
|
764 | scrollIntoView: true,
|
765 | effects: announceMatch(view, next),
|
766 | userEvent: "select.search"
|
767 | });
|
768 | return true;
|
769 | });
|
770 |
|
771 |
|
772 |
|
773 |
|
774 |
|
775 | const findPrevious = searchCommand((view, { query }) => {
|
776 | let { state } = view, { from, to } = state.selection.main;
|
777 | let range = query.prevMatch(state.doc, from, to);
|
778 | if (!range)
|
779 | return false;
|
780 | view.dispatch({
|
781 | selection: { anchor: range.from, head: range.to },
|
782 | scrollIntoView: true,
|
783 | effects: announceMatch(view, range),
|
784 | userEvent: "select.search"
|
785 | });
|
786 | return true;
|
787 | });
|
788 |
|
789 |
|
790 |
|
791 | const selectMatches = searchCommand((view, { query }) => {
|
792 | let ranges = query.matchAll(view.state.doc, 1000);
|
793 | if (!ranges || !ranges.length)
|
794 | return false;
|
795 | view.dispatch({
|
796 | selection: EditorSelection.create(ranges.map(r => EditorSelection.range(r.from, r.to))),
|
797 | userEvent: "select.search.matches"
|
798 | });
|
799 | return true;
|
800 | });
|
801 |
|
802 |
|
803 |
|
804 | const selectSelectionMatches = ({ state, dispatch }) => {
|
805 | let sel = state.selection;
|
806 | if (sel.ranges.length > 1 || sel.main.empty)
|
807 | return false;
|
808 | let { from, to } = sel.main;
|
809 | let ranges = [], main = 0;
|
810 | for (let cur = new SearchCursor(state.doc, state.sliceDoc(from, to)); !cur.next().done;) {
|
811 | if (ranges.length > 1000)
|
812 | return false;
|
813 | if (cur.value.from == from)
|
814 | main = ranges.length;
|
815 | ranges.push(EditorSelection.range(cur.value.from, cur.value.to));
|
816 | }
|
817 | dispatch(state.update({
|
818 | selection: EditorSelection.create(ranges, main),
|
819 | userEvent: "select.search.matches"
|
820 | }));
|
821 | return true;
|
822 | };
|
823 |
|
824 |
|
825 |
|
826 | const replaceNext = searchCommand((view, { query }) => {
|
827 | let { state } = view, { from, to } = state.selection.main;
|
828 | if (state.readOnly)
|
829 | return false;
|
830 | let next = query.nextMatch(state.doc, from, from);
|
831 | if (!next)
|
832 | return false;
|
833 | let changes = [], selection, replacement;
|
834 | if (next.from == from && next.to == to) {
|
835 | replacement = state.toText(query.getReplacement(next));
|
836 | changes.push({ from: next.from, to: next.to, insert: replacement });
|
837 | next = query.nextMatch(state.doc, next.from, next.to);
|
838 | }
|
839 | if (next) {
|
840 | let off = changes.length == 0 || changes[0].from >= next.to ? 0 : next.to - next.from - replacement.length;
|
841 | selection = { anchor: next.from - off, head: next.to - off };
|
842 | }
|
843 | view.dispatch({
|
844 | changes, selection,
|
845 | scrollIntoView: !!selection,
|
846 | effects: next ? announceMatch(view, next) : undefined,
|
847 | userEvent: "input.replace"
|
848 | });
|
849 | return true;
|
850 | });
|
851 |
|
852 |
|
853 |
|
854 |
|
855 | const replaceAll = searchCommand((view, { query }) => {
|
856 | if (view.state.readOnly)
|
857 | return false;
|
858 | let changes = query.matchAll(view.state.doc, 1e9).map(match => {
|
859 | let { from, to } = match;
|
860 | return { from, to, insert: query.getReplacement(match) };
|
861 | });
|
862 | if (!changes.length)
|
863 | return false;
|
864 | view.dispatch({
|
865 | changes,
|
866 | userEvent: "input.replace.all"
|
867 | });
|
868 | return true;
|
869 | });
|
870 | function createSearchPanel(view) {
|
871 | return view.state.facet(searchConfigFacet).createPanel(view);
|
872 | }
|
873 | function defaultQuery(state, fallback) {
|
874 | var _a;
|
875 | let sel = state.selection.main;
|
876 | let selText = sel.empty || sel.to > sel.from + 100 ? "" : state.sliceDoc(sel.from, sel.to);
|
877 | let caseSensitive = (_a = fallback === null || fallback === void 0 ? void 0 : fallback.caseSensitive) !== null && _a !== void 0 ? _a : state.facet(searchConfigFacet).caseSensitive;
|
878 | return fallback && !selText ? fallback : new SearchQuery({ search: selText.replace(/\n/g, "\\n"), caseSensitive });
|
879 | }
|
880 |
|
881 |
|
882 |
|
883 | const openSearchPanel = view => {
|
884 | let state = view.state.field(searchState, false);
|
885 | if (state && state.panel) {
|
886 | let panel = getPanel(view, createSearchPanel);
|
887 | if (!panel)
|
888 | return false;
|
889 | let searchInput = panel.dom.querySelector("[name=search]");
|
890 | if (searchInput != view.root.activeElement) {
|
891 | let query = defaultQuery(view.state, state.query.spec);
|
892 | if (query.valid)
|
893 | view.dispatch({ effects: setSearchQuery.of(query) });
|
894 | searchInput.focus();
|
895 | searchInput.select();
|
896 | }
|
897 | }
|
898 | else {
|
899 | view.dispatch({ effects: [
|
900 | togglePanel.of(true),
|
901 | state ? setSearchQuery.of(defaultQuery(view.state, state.query.spec)) : StateEffect.appendConfig.of(searchExtensions)
|
902 | ] });
|
903 | }
|
904 | return true;
|
905 | };
|
906 |
|
907 |
|
908 |
|
909 | const closeSearchPanel = view => {
|
910 | let state = view.state.field(searchState, false);
|
911 | if (!state || !state.panel)
|
912 | return false;
|
913 | let panel = getPanel(view, createSearchPanel);
|
914 | if (panel && panel.dom.contains(view.root.activeElement))
|
915 | view.focus();
|
916 | view.dispatch({ effects: togglePanel.of(false) });
|
917 | return true;
|
918 | };
|
919 |
|
920 |
|
921 |
|
922 |
|
923 |
|
924 |
|
925 |
|
926 |
|
927 |
|
928 | const searchKeymap = [
|
929 | { key: "Mod-f", run: openSearchPanel, scope: "editor search-panel" },
|
930 | { key: "F3", run: findNext, shift: findPrevious, scope: "editor search-panel", preventDefault: true },
|
931 | { key: "Mod-g", run: findNext, shift: findPrevious, scope: "editor search-panel", preventDefault: true },
|
932 | { key: "Escape", run: closeSearchPanel, scope: "editor search-panel" },
|
933 | { key: "Mod-Shift-l", run: selectSelectionMatches },
|
934 | { key: "Alt-g", run: gotoLine },
|
935 | { key: "Mod-d", run: selectNextOccurrence, preventDefault: true },
|
936 | ];
|
937 | class SearchPanel {
|
938 | constructor(view) {
|
939 | this.view = view;
|
940 | let query = this.query = view.state.field(searchState).query.spec;
|
941 | this.commit = this.commit.bind(this);
|
942 | this.searchField = elt("input", {
|
943 | value: query.search,
|
944 | placeholder: phrase(view, "Find"),
|
945 | "aria-label": phrase(view, "Find"),
|
946 | class: "cm-textfield",
|
947 | name: "search",
|
948 | onchange: this.commit,
|
949 | onkeyup: this.commit
|
950 | });
|
951 | this.replaceField = elt("input", {
|
952 | value: query.replace,
|
953 | placeholder: phrase(view, "Replace"),
|
954 | "aria-label": phrase(view, "Replace"),
|
955 | class: "cm-textfield",
|
956 | name: "replace",
|
957 | onchange: this.commit,
|
958 | onkeyup: this.commit
|
959 | });
|
960 | this.caseField = elt("input", {
|
961 | type: "checkbox",
|
962 | name: "case",
|
963 | checked: query.caseSensitive,
|
964 | onchange: this.commit
|
965 | });
|
966 | this.reField = elt("input", {
|
967 | type: "checkbox",
|
968 | name: "re",
|
969 | checked: query.regexp,
|
970 | onchange: this.commit
|
971 | });
|
972 | function button(name, onclick, content) {
|
973 | return elt("button", { class: "cm-button", name, onclick, type: "button" }, content);
|
974 | }
|
975 | this.dom = elt("div", { onkeydown: (e) => this.keydown(e), class: "cm-search" }, [
|
976 | this.searchField,
|
977 | button("next", () => findNext(view), [phrase(view, "next")]),
|
978 | button("prev", () => findPrevious(view), [phrase(view, "previous")]),
|
979 | button("select", () => selectMatches(view), [phrase(view, "all")]),
|
980 | elt("label", null, [this.caseField, phrase(view, "match case")]),
|
981 | elt("label", null, [this.reField, phrase(view, "regexp")]),
|
982 | ...view.state.readOnly ? [] : [
|
983 | elt("br"),
|
984 | this.replaceField,
|
985 | button("replace", () => replaceNext(view), [phrase(view, "replace")]),
|
986 | button("replaceAll", () => replaceAll(view), [phrase(view, "replace all")]),
|
987 | elt("button", {
|
988 | name: "close",
|
989 | onclick: () => closeSearchPanel(view),
|
990 | "aria-label": phrase(view, "close"),
|
991 | type: "button"
|
992 | }, ["×"])
|
993 | ]
|
994 | ]);
|
995 | }
|
996 | commit() {
|
997 | let query = new SearchQuery({
|
998 | search: this.searchField.value,
|
999 | caseSensitive: this.caseField.checked,
|
1000 | regexp: this.reField.checked,
|
1001 | replace: this.replaceField.value
|
1002 | });
|
1003 | if (!query.eq(this.query)) {
|
1004 | this.query = query;
|
1005 | this.view.dispatch({ effects: setSearchQuery.of(query) });
|
1006 | }
|
1007 | }
|
1008 | keydown(e) {
|
1009 | if (runScopeHandlers(this.view, e, "search-panel")) {
|
1010 | e.preventDefault();
|
1011 | }
|
1012 | else if (e.keyCode == 13 && e.target == this.searchField) {
|
1013 | e.preventDefault();
|
1014 | (e.shiftKey ? findPrevious : findNext)(this.view);
|
1015 | }
|
1016 | else if (e.keyCode == 13 && e.target == this.replaceField) {
|
1017 | e.preventDefault();
|
1018 | replaceNext(this.view);
|
1019 | }
|
1020 | }
|
1021 | update(update) {
|
1022 | for (let tr of update.transactions)
|
1023 | for (let effect of tr.effects) {
|
1024 | if (effect.is(setSearchQuery) && !effect.value.eq(this.query))
|
1025 | this.setQuery(effect.value);
|
1026 | }
|
1027 | }
|
1028 | setQuery(query) {
|
1029 | this.query = query;
|
1030 | this.searchField.value = query.search;
|
1031 | this.replaceField.value = query.replace;
|
1032 | this.caseField.checked = query.caseSensitive;
|
1033 | this.reField.checked = query.regexp;
|
1034 | }
|
1035 | mount() {
|
1036 | this.searchField.select();
|
1037 | }
|
1038 | get pos() { return 80; }
|
1039 | get top() { return this.view.state.facet(searchConfigFacet).top; }
|
1040 | }
|
1041 | function phrase(view, phrase) { return view.state.phrase(phrase); }
|
1042 | const AnnounceMargin = 30;
|
1043 | const Break = /[\s\.,:;?!]/;
|
1044 | function announceMatch(view, { from, to }) {
|
1045 | let lineStart = view.state.doc.lineAt(from).from, lineEnd = view.state.doc.lineAt(to).to;
|
1046 | let start = Math.max(lineStart, from - AnnounceMargin), end = Math.min(lineEnd, to + AnnounceMargin);
|
1047 | let text = view.state.sliceDoc(start, end);
|
1048 | if (start != lineStart) {
|
1049 | for (let i = 0; i < AnnounceMargin; i++)
|
1050 | if (!Break.test(text[i + 1]) && Break.test(text[i])) {
|
1051 | text = text.slice(i);
|
1052 | break;
|
1053 | }
|
1054 | }
|
1055 | if (end != lineEnd) {
|
1056 | for (let i = text.length - 1; i > text.length - AnnounceMargin; i--)
|
1057 | if (!Break.test(text[i - 1]) && Break.test(text[i])) {
|
1058 | text = text.slice(0, i);
|
1059 | break;
|
1060 | }
|
1061 | }
|
1062 | return EditorView.announce.of(`${view.state.phrase("current match")}. ${text} ${view.state.phrase("on line")} ${view.state.doc.lineAt(from).number}`);
|
1063 | }
|
1064 | const baseTheme = EditorView.baseTheme({
|
1065 | ".cm-panel.cm-search": {
|
1066 | padding: "2px 6px 4px",
|
1067 | position: "relative",
|
1068 | "& [name=close]": {
|
1069 | position: "absolute",
|
1070 | top: "0",
|
1071 | right: "4px",
|
1072 | backgroundColor: "inherit",
|
1073 | border: "none",
|
1074 | font: "inherit",
|
1075 | padding: 0,
|
1076 | margin: 0
|
1077 | },
|
1078 | "& input, & button, & label": {
|
1079 | margin: ".2em .6em .2em 0"
|
1080 | },
|
1081 | "& input[type=checkbox]": {
|
1082 | marginRight: ".2em"
|
1083 | },
|
1084 | "& label": {
|
1085 | fontSize: "80%",
|
1086 | whiteSpace: "pre"
|
1087 | }
|
1088 | },
|
1089 | "&light .cm-searchMatch": { backgroundColor: "#ffff0054" },
|
1090 | "&dark .cm-searchMatch": { backgroundColor: "#00ffff8a" },
|
1091 | "&light .cm-searchMatch-selected": { backgroundColor: "#ff6a0054" },
|
1092 | "&dark .cm-searchMatch-selected": { backgroundColor: "#ff00ff8a" }
|
1093 | });
|
1094 | const searchExtensions = [
|
1095 | searchState,
|
1096 | Prec.lowest(searchHighlighter),
|
1097 | baseTheme
|
1098 | ];
|
1099 |
|
1100 | export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchConfig, searchKeymap, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
|