123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125 |
- import { showPanel, EditorView, getPanel, Decoration, ViewPlugin, runScopeHandlers } from '@codemirror/view';
- import { codePointAt, fromCodePoint, codePointSize, StateEffect, StateField, EditorSelection, Facet, combineConfig, CharCategory, RangeSetBuilder, Prec } from '@codemirror/state';
- import elt from 'crelt';
- const basicNormalize = typeof String.prototype.normalize == "function"
- ? x => x.normalize("NFKD") : x => x;
- /**
- A search cursor provides an iterator over text matches in a
- document.
- */
- class SearchCursor {
- /**
- Create a text cursor. The query is the search string, `from` to
- `to` provides the region to search.
-
- When `normalize` is given, it will be called, on both the query
- string and the content it is matched against, before comparing.
- You can, for example, create a case-insensitive search by
- passing `s => s.toLowerCase()`.
-
- Text is always normalized with
- [`.normalize("NFKD")`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize)
- (when supported).
- */
- constructor(text, query, from = 0, to = text.length, normalize) {
- /**
- The current match (only holds a meaningful value after
- [`next`](https://codemirror.net/6/docs/ref/#search.SearchCursor.next) has been called and when
- `done` is false).
- */
- this.value = { from: 0, to: 0 };
- /**
- Whether the end of the iterated region has been reached.
- */
- this.done = false;
- this.matches = [];
- this.buffer = "";
- this.bufferPos = 0;
- this.iter = text.iterRange(from, to);
- this.bufferStart = from;
- this.normalize = normalize ? x => normalize(basicNormalize(x)) : basicNormalize;
- this.query = this.normalize(query);
- }
- peek() {
- if (this.bufferPos == this.buffer.length) {
- this.bufferStart += this.buffer.length;
- this.iter.next();
- if (this.iter.done)
- return -1;
- this.bufferPos = 0;
- this.buffer = this.iter.value;
- }
- return codePointAt(this.buffer, this.bufferPos);
- }
- /**
- Look for the next match. Updates the iterator's
- [`value`](https://codemirror.net/6/docs/ref/#search.SearchCursor.value) and
- [`done`](https://codemirror.net/6/docs/ref/#search.SearchCursor.done) properties. Should be called
- at least once before using the cursor.
- */
- next() {
- while (this.matches.length)
- this.matches.pop();
- return this.nextOverlapping();
- }
- /**
- The `next` method will ignore matches that partially overlap a
- previous match. This method behaves like `next`, but includes
- such matches.
- */
- nextOverlapping() {
- for (;;) {
- let next = this.peek();
- if (next < 0) {
- this.done = true;
- return this;
- }
- let str = fromCodePoint(next), start = this.bufferStart + this.bufferPos;
- this.bufferPos += codePointSize(next);
- let norm = this.normalize(str);
- for (let i = 0, pos = start;; i++) {
- let code = norm.charCodeAt(i);
- let match = this.match(code, pos);
- if (match) {
- this.value = match;
- return this;
- }
- if (i == norm.length - 1)
- break;
- if (pos == start && i < str.length && str.charCodeAt(i) == code)
- pos++;
- }
- }
- }
- match(code, pos) {
- let match = null;
- for (let i = 0; i < this.matches.length; i += 2) {
- let index = this.matches[i], keep = false;
- if (this.query.charCodeAt(index) == code) {
- if (index == this.query.length - 1) {
- match = { from: this.matches[i + 1], to: pos + 1 };
- }
- else {
- this.matches[i]++;
- keep = true;
- }
- }
- if (!keep) {
- this.matches.splice(i, 2);
- i -= 2;
- }
- }
- if (this.query.charCodeAt(0) == code) {
- if (this.query.length == 1)
- match = { from: pos, to: pos + 1 };
- else
- this.matches.push(1, pos);
- }
- return match;
- }
- }
- if (typeof Symbol != "undefined")
- SearchCursor.prototype[Symbol.iterator] = function () { return this; };
- const empty = { from: -1, to: -1, match: /*@__PURE__*//.*/.exec("") };
- const baseFlags = "gm" + (/x/.unicode == null ? "" : "u");
- /**
- This class is similar to [`SearchCursor`](https://codemirror.net/6/docs/ref/#search.SearchCursor)
- but searches for a regular expression pattern instead of a plain
- string.
- */
- class RegExpCursor {
- /**
- Create a cursor that will search the given range in the given
- document. `query` should be the raw pattern (as you'd pass it to
- `new RegExp`).
- */
- constructor(text, query, options, from = 0, to = text.length) {
- this.to = to;
- this.curLine = "";
- /**
- Set to `true` when the cursor has reached the end of the search
- range.
- */
- this.done = false;
- /**
- Will contain an object with the extent of the match and the
- match object when [`next`](https://codemirror.net/6/docs/ref/#search.RegExpCursor.next)
- sucessfully finds a match.
- */
- this.value = empty;
- if (/\\[sWDnr]|\n|\r|\[\^/.test(query))
- return new MultilineRegExpCursor(text, query, options, from, to);
- this.re = new RegExp(query, baseFlags + ((options === null || options === void 0 ? void 0 : options.ignoreCase) ? "i" : ""));
- this.iter = text.iter();
- let startLine = text.lineAt(from);
- this.curLineStart = startLine.from;
- this.matchPos = from;
- this.getLine(this.curLineStart);
- }
- getLine(skip) {
- this.iter.next(skip);
- if (this.iter.lineBreak) {
- this.curLine = "";
- }
- else {
- this.curLine = this.iter.value;
- if (this.curLineStart + this.curLine.length > this.to)
- this.curLine = this.curLine.slice(0, this.to - this.curLineStart);
- this.iter.next();
- }
- }
- nextLine() {
- this.curLineStart = this.curLineStart + this.curLine.length + 1;
- if (this.curLineStart > this.to)
- this.curLine = "";
- else
- this.getLine(0);
- }
- /**
- Move to the next match, if there is one.
- */
- next() {
- for (let off = this.matchPos - this.curLineStart;;) {
- this.re.lastIndex = off;
- let match = this.matchPos <= this.to && this.re.exec(this.curLine);
- if (match) {
- let from = this.curLineStart + match.index, to = from + match[0].length;
- this.matchPos = to + (from == to ? 1 : 0);
- if (from == this.curLine.length)
- this.nextLine();
- if (from < to || from > this.value.to) {
- this.value = { from, to, match };
- return this;
- }
- off = this.matchPos - this.curLineStart;
- }
- else if (this.curLineStart + this.curLine.length < this.to) {
- this.nextLine();
- off = 0;
- }
- else {
- this.done = true;
- return this;
- }
- }
- }
- }
- const flattened = /*@__PURE__*/new WeakMap();
- // Reusable (partially) flattened document strings
- class FlattenedDoc {
- constructor(from, text) {
- this.from = from;
- this.text = text;
- }
- get to() { return this.from + this.text.length; }
- static get(doc, from, to) {
- let cached = flattened.get(doc);
- if (!cached || cached.from >= to || cached.to <= from) {
- let flat = new FlattenedDoc(from, doc.sliceString(from, to));
- flattened.set(doc, flat);
- return flat;
- }
- if (cached.from == from && cached.to == to)
- return cached;
- let { text, from: cachedFrom } = cached;
- if (cachedFrom > from) {
- text = doc.sliceString(from, cachedFrom) + text;
- cachedFrom = from;
- }
- if (cached.to < to)
- text += doc.sliceString(cached.to, to);
- flattened.set(doc, new FlattenedDoc(cachedFrom, text));
- return new FlattenedDoc(from, text.slice(from - cachedFrom, to - cachedFrom));
- }
- }
- class MultilineRegExpCursor {
- constructor(text, query, options, from, to) {
- this.text = text;
- this.to = to;
- this.done = false;
- this.value = empty;
- this.matchPos = from;
- this.re = new RegExp(query, baseFlags + ((options === null || options === void 0 ? void 0 : options.ignoreCase) ? "i" : ""));
- this.flat = FlattenedDoc.get(text, from, this.chunkEnd(from + 5000 /* Base */));
- }
- chunkEnd(pos) {
- return pos >= this.to ? this.to : this.text.lineAt(pos).to;
- }
- next() {
- for (;;) {
- let off = this.re.lastIndex = this.matchPos - this.flat.from;
- let match = this.re.exec(this.flat.text);
- // Skip empty matches directly after the last match
- if (match && !match[0] && match.index == off) {
- this.re.lastIndex = off + 1;
- match = this.re.exec(this.flat.text);
- }
- // If a match goes almost to the end of a noncomplete chunk, try
- // again, since it'll likely be able to match more
- if (match && this.flat.to < this.to && match.index + match[0].length > this.flat.text.length - 10)
- match = null;
- if (match) {
- let from = this.flat.from + match.index, to = from + match[0].length;
- this.value = { from, to, match };
- this.matchPos = to + (from == to ? 1 : 0);
- return this;
- }
- else {
- if (this.flat.to == this.to) {
- this.done = true;
- return this;
- }
- // Grow the flattened doc
- this.flat = FlattenedDoc.get(this.text, this.flat.from, this.chunkEnd(this.flat.from + this.flat.text.length * 2));
- }
- }
- }
- }
- if (typeof Symbol != "undefined") {
- RegExpCursor.prototype[Symbol.iterator] = MultilineRegExpCursor.prototype[Symbol.iterator] =
- function () { return this; };
- }
- function validRegExp(source) {
- try {
- new RegExp(source, baseFlags);
- return true;
- }
- catch (_a) {
- return false;
- }
- }
- function createLineDialog(view) {
- let input = elt("input", { class: "cm-textfield", name: "line" });
- let dom = elt("form", {
- class: "cm-gotoLine",
- onkeydown: (event) => {
- if (event.keyCode == 27) { // Escape
- event.preventDefault();
- view.dispatch({ effects: dialogEffect.of(false) });
- view.focus();
- }
- else if (event.keyCode == 13) { // Enter
- event.preventDefault();
- go();
- }
- },
- onsubmit: (event) => {
- event.preventDefault();
- go();
- }
- }, elt("label", view.state.phrase("Go to line"), ": ", input), " ", elt("button", { class: "cm-button", type: "submit" }, view.state.phrase("go")));
- function go() {
- let match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(input.value);
- if (!match)
- return;
- let { state } = view, startLine = state.doc.lineAt(state.selection.main.head);
- let [, sign, ln, cl, percent] = match;
- let col = cl ? +cl.slice(1) : 0;
- let line = ln ? +ln : startLine.number;
- if (ln && percent) {
- let pc = line / 100;
- if (sign)
- pc = pc * (sign == "-" ? -1 : 1) + (startLine.number / state.doc.lines);
- line = Math.round(state.doc.lines * pc);
- }
- else if (ln && sign) {
- line = line * (sign == "-" ? -1 : 1) + startLine.number;
- }
- let docLine = state.doc.line(Math.max(1, Math.min(state.doc.lines, line)));
- view.dispatch({
- effects: dialogEffect.of(false),
- selection: EditorSelection.cursor(docLine.from + Math.max(0, Math.min(col, docLine.length))),
- scrollIntoView: true
- });
- view.focus();
- }
- return { dom };
- }
- const dialogEffect = /*@__PURE__*/StateEffect.define();
- const dialogField = /*@__PURE__*/StateField.define({
- create() { return true; },
- update(value, tr) {
- for (let e of tr.effects)
- if (e.is(dialogEffect))
- value = e.value;
- return value;
- },
- provide: f => showPanel.from(f, val => val ? createLineDialog : null)
- });
- /**
- Command that shows a dialog asking the user for a line number, and
- when a valid position is provided, moves the cursor to that line.
- Supports line numbers, relative line offsets prefixed with `+` or
- `-`, document percentages suffixed with `%`, and an optional
- column position by adding `:` and a second number after the line
- number.
- The dialog can be styled with the `panel.gotoLine` theme
- selector.
- */
- const gotoLine = view => {
- let panel = getPanel(view, createLineDialog);
- if (!panel) {
- let effects = [dialogEffect.of(true)];
- if (view.state.field(dialogField, false) == null)
- effects.push(StateEffect.appendConfig.of([dialogField, baseTheme$1]));
- view.dispatch({ effects });
- panel = getPanel(view, createLineDialog);
- }
- if (panel)
- panel.dom.querySelector("input").focus();
- return true;
- };
- const baseTheme$1 = /*@__PURE__*/EditorView.baseTheme({
- ".cm-panel.cm-gotoLine": {
- padding: "2px 6px 4px",
- "& label": { fontSize: "80%" }
- }
- });
- const defaultHighlightOptions = {
- highlightWordAroundCursor: false,
- minSelectionLength: 1,
- maxMatches: 100,
- wholeWords: false
- };
- const highlightConfig = /*@__PURE__*/Facet.define({
- combine(options) {
- return combineConfig(options, defaultHighlightOptions, {
- highlightWordAroundCursor: (a, b) => a || b,
- minSelectionLength: Math.min,
- maxMatches: Math.min
- });
- }
- });
- /**
- This extension highlights text that matches the selection. It uses
- the `"cm-selectionMatch"` class for the highlighting. When
- `highlightWordAroundCursor` is enabled, the word at the cursor
- itself will be highlighted with `"cm-selectionMatch-main"`.
- */
- function highlightSelectionMatches(options) {
- let ext = [defaultTheme, matchHighlighter];
- if (options)
- ext.push(highlightConfig.of(options));
- return ext;
- }
- const matchDeco = /*@__PURE__*/Decoration.mark({ class: "cm-selectionMatch" });
- const mainMatchDeco = /*@__PURE__*/Decoration.mark({ class: "cm-selectionMatch cm-selectionMatch-main" });
- // Whether the characters directly outside the given positions are non-word characters
- function insideWordBoundaries(check, state, from, to) {
- return (from == 0 || check(state.sliceDoc(from - 1, from)) != CharCategory.Word) &&
- (to == state.doc.length || check(state.sliceDoc(to, to + 1)) != CharCategory.Word);
- }
- // Whether the characters directly at the given positions are word characters
- function insideWord(check, state, from, to) {
- return check(state.sliceDoc(from, from + 1)) == CharCategory.Word
- && check(state.sliceDoc(to - 1, to)) == CharCategory.Word;
- }
- const matchHighlighter = /*@__PURE__*/ViewPlugin.fromClass(class {
- constructor(view) {
- this.decorations = this.getDeco(view);
- }
- update(update) {
- if (update.selectionSet || update.docChanged || update.viewportChanged)
- this.decorations = this.getDeco(update.view);
- }
- getDeco(view) {
- let conf = view.state.facet(highlightConfig);
- let { state } = view, sel = state.selection;
- if (sel.ranges.length > 1)
- return Decoration.none;
- let range = sel.main, query, check = null;
- if (range.empty) {
- if (!conf.highlightWordAroundCursor)
- return Decoration.none;
- let word = state.wordAt(range.head);
- if (!word)
- return Decoration.none;
- check = state.charCategorizer(range.head);
- query = state.sliceDoc(word.from, word.to);
- }
- else {
- let len = range.to - range.from;
- if (len < conf.minSelectionLength || len > 200)
- return Decoration.none;
- if (conf.wholeWords) {
- query = state.sliceDoc(range.from, range.to); // TODO: allow and include leading/trailing space?
- check = state.charCategorizer(range.head);
- if (!(insideWordBoundaries(check, state, range.from, range.to)
- && insideWord(check, state, range.from, range.to)))
- return Decoration.none;
- }
- else {
- query = state.sliceDoc(range.from, range.to).trim();
- if (!query)
- return Decoration.none;
- }
- }
- let deco = [];
- for (let part of view.visibleRanges) {
- let cursor = new SearchCursor(state.doc, query, part.from, part.to);
- while (!cursor.next().done) {
- let { from, to } = cursor.value;
- if (!check || insideWordBoundaries(check, state, from, to)) {
- if (range.empty && from <= range.from && to >= range.to)
- deco.push(mainMatchDeco.range(from, to));
- else if (from >= range.to || to <= range.from)
- deco.push(matchDeco.range(from, to));
- if (deco.length > conf.maxMatches)
- return Decoration.none;
- }
- }
- }
- return Decoration.set(deco);
- }
- }, {
- decorations: v => v.decorations
- });
- const defaultTheme = /*@__PURE__*/EditorView.baseTheme({
- ".cm-selectionMatch": { backgroundColor: "#99ff7780" },
- ".cm-searchMatch .cm-selectionMatch": { backgroundColor: "transparent" }
- });
- // Select the words around the cursors.
- const selectWord = ({ state, dispatch }) => {
- let { selection } = state;
- let newSel = EditorSelection.create(selection.ranges.map(range => state.wordAt(range.head) || EditorSelection.cursor(range.head)), selection.mainIndex);
- if (newSel.eq(selection))
- return false;
- dispatch(state.update({ selection: newSel }));
- return true;
- };
- // Find next occurrence of query relative to last cursor. Wrap around
- // the document if there are no more matches.
- function findNextOccurrence(state, query) {
- let { main, ranges } = state.selection;
- let word = state.wordAt(main.head), fullWord = word && word.from == main.from && word.to == main.to;
- for (let cycled = false, cursor = new SearchCursor(state.doc, query, ranges[ranges.length - 1].to);;) {
- cursor.next();
- if (cursor.done) {
- if (cycled)
- return null;
- cursor = new SearchCursor(state.doc, query, 0, Math.max(0, ranges[ranges.length - 1].from - 1));
- cycled = true;
- }
- else {
- if (cycled && ranges.some(r => r.from == cursor.value.from))
- continue;
- if (fullWord) {
- let word = state.wordAt(cursor.value.from);
- if (!word || word.from != cursor.value.from || word.to != cursor.value.to)
- continue;
- }
- return cursor.value;
- }
- }
- }
- /**
- Select next occurrence of the current selection. Expand selection
- to the surrounding word when the selection is empty.
- */
- const selectNextOccurrence = ({ state, dispatch }) => {
- let { ranges } = state.selection;
- if (ranges.some(sel => sel.from === sel.to))
- return selectWord({ state, dispatch });
- let searchedText = state.sliceDoc(ranges[0].from, ranges[0].to);
- if (state.selection.ranges.some(r => state.sliceDoc(r.from, r.to) != searchedText))
- return false;
- let range = findNextOccurrence(state, searchedText);
- if (!range)
- return false;
- dispatch(state.update({
- selection: state.selection.addRange(EditorSelection.range(range.from, range.to), false),
- effects: EditorView.scrollIntoView(range.to)
- }));
- return true;
- };
- const searchConfigFacet = /*@__PURE__*/Facet.define({
- combine(configs) {
- var _a;
- return {
- top: configs.reduce((val, conf) => val !== null && val !== void 0 ? val : conf.top, undefined) || false,
- caseSensitive: configs.reduce((val, conf) => val !== null && val !== void 0 ? val : conf.caseSensitive, undefined) || false,
- createPanel: ((_a = configs.find(c => c.createPanel)) === null || _a === void 0 ? void 0 : _a.createPanel) || (view => new SearchPanel(view))
- };
- }
- });
- /**
- Add search state to the editor configuration, and optionally
- configure the search extension.
- ([`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel) will automatically
- enable this if it isn't already on).
- */
- function search(config) {
- return config ? [searchConfigFacet.of(config), searchExtensions] : searchExtensions;
- }
- /**
- A search query. Part of the editor's search state.
- */
- class SearchQuery {
- /**
- Create a query object.
- */
- constructor(config) {
- this.search = config.search;
- this.caseSensitive = !!config.caseSensitive;
- this.regexp = !!config.regexp;
- this.replace = config.replace || "";
- this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
- this.unquoted = config.literal ? this.search : this.search.replace(/\\([nrt\\])/g, (_, ch) => ch == "n" ? "\n" : ch == "r" ? "\r" : ch == "t" ? "\t" : "\\");
- }
- /**
- Compare this query to another query.
- */
- eq(other) {
- return this.search == other.search && this.replace == other.replace &&
- this.caseSensitive == other.caseSensitive && this.regexp == other.regexp;
- }
- /**
- @internal
- */
- create() {
- return this.regexp ? new RegExpQuery(this) : new StringQuery(this);
- }
- /**
- Get a search cursor for this query, searching through the given
- range in the given document.
- */
- getCursor(doc, from = 0, to = doc.length) {
- return this.regexp ? regexpCursor(this, doc, from, to) : stringCursor(this, doc, from, to);
- }
- }
- class QueryType {
- constructor(spec) {
- this.spec = spec;
- }
- }
- function stringCursor(spec, doc, from, to) {
- return new SearchCursor(doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase());
- }
- class StringQuery extends QueryType {
- constructor(spec) {
- super(spec);
- }
- nextMatch(doc, curFrom, curTo) {
- let cursor = stringCursor(this.spec, doc, curTo, doc.length).nextOverlapping();
- if (cursor.done)
- cursor = stringCursor(this.spec, doc, 0, curFrom).nextOverlapping();
- return cursor.done ? null : cursor.value;
- }
- // Searching in reverse is, rather than implementing inverted search
- // cursor, done by scanning chunk after chunk forward.
- prevMatchInRange(doc, from, to) {
- for (let pos = to;;) {
- let start = Math.max(from, pos - 10000 /* ChunkSize */ - this.spec.unquoted.length);
- let cursor = stringCursor(this.spec, doc, start, pos), range = null;
- while (!cursor.nextOverlapping().done)
- range = cursor.value;
- if (range)
- return range;
- if (start == from)
- return null;
- pos -= 10000 /* ChunkSize */;
- }
- }
- prevMatch(doc, curFrom, curTo) {
- return this.prevMatchInRange(doc, 0, curFrom) ||
- this.prevMatchInRange(doc, curTo, doc.length);
- }
- getReplacement(_result) { return this.spec.replace; }
- matchAll(doc, limit) {
- let cursor = stringCursor(this.spec, doc, 0, doc.length), ranges = [];
- while (!cursor.next().done) {
- if (ranges.length >= limit)
- return null;
- ranges.push(cursor.value);
- }
- return ranges;
- }
- highlight(doc, from, to, add) {
- let cursor = stringCursor(this.spec, doc, Math.max(0, from - this.spec.unquoted.length), Math.min(to + this.spec.unquoted.length, doc.length));
- while (!cursor.next().done)
- add(cursor.value.from, cursor.value.to);
- }
- }
- function regexpCursor(spec, doc, from, to) {
- return new RegExpCursor(doc, spec.search, spec.caseSensitive ? undefined : { ignoreCase: true }, from, to);
- }
- class RegExpQuery extends QueryType {
- nextMatch(doc, curFrom, curTo) {
- let cursor = regexpCursor(this.spec, doc, curTo, doc.length).next();
- if (cursor.done)
- cursor = regexpCursor(this.spec, doc, 0, curFrom).next();
- return cursor.done ? null : cursor.value;
- }
- prevMatchInRange(doc, from, to) {
- for (let size = 1;; size++) {
- let start = Math.max(from, to - size * 10000 /* ChunkSize */);
- let cursor = regexpCursor(this.spec, doc, start, to), range = null;
- while (!cursor.next().done)
- range = cursor.value;
- if (range && (start == from || range.from > start + 10))
- return range;
- if (start == from)
- return null;
- }
- }
- prevMatch(doc, curFrom, curTo) {
- return this.prevMatchInRange(doc, 0, curFrom) ||
- this.prevMatchInRange(doc, curTo, doc.length);
- }
- getReplacement(result) {
- return this.spec.replace.replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
- : i == "&" ? result.match[0]
- : i != "0" && +i < result.match.length ? result.match[i]
- : m);
- }
- matchAll(doc, limit) {
- let cursor = regexpCursor(this.spec, doc, 0, doc.length), ranges = [];
- while (!cursor.next().done) {
- if (ranges.length >= limit)
- return null;
- ranges.push(cursor.value);
- }
- return ranges;
- }
- highlight(doc, from, to, add) {
- let cursor = regexpCursor(this.spec, doc, Math.max(0, from - 250 /* HighlightMargin */), Math.min(to + 250 /* HighlightMargin */, doc.length));
- while (!cursor.next().done)
- add(cursor.value.from, cursor.value.to);
- }
- }
- /**
- A state effect that updates the current search query. Note that
- this only has an effect if the search state has been initialized
- (by including [`search`](https://codemirror.net/6/docs/ref/#search.search) in your configuration or
- by running [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel) at least
- once).
- */
- const setSearchQuery = /*@__PURE__*/StateEffect.define();
- const togglePanel = /*@__PURE__*/StateEffect.define();
- const searchState = /*@__PURE__*/StateField.define({
- create(state) {
- return new SearchState(defaultQuery(state).create(), null);
- },
- update(value, tr) {
- for (let effect of tr.effects) {
- if (effect.is(setSearchQuery))
- value = new SearchState(effect.value.create(), value.panel);
- else if (effect.is(togglePanel))
- value = new SearchState(value.query, effect.value ? createSearchPanel : null);
- }
- return value;
- },
- provide: f => showPanel.from(f, val => val.panel)
- });
- /**
- Get the current search query from an editor state.
- */
- function getSearchQuery(state) {
- let curState = state.field(searchState, false);
- return curState ? curState.query.spec : defaultQuery(state);
- }
- class SearchState {
- constructor(query, panel) {
- this.query = query;
- this.panel = panel;
- }
- }
- const matchMark = /*@__PURE__*/Decoration.mark({ class: "cm-searchMatch" }), selectedMatchMark = /*@__PURE__*/Decoration.mark({ class: "cm-searchMatch cm-searchMatch-selected" });
- const searchHighlighter = /*@__PURE__*/ViewPlugin.fromClass(class {
- constructor(view) {
- this.view = view;
- this.decorations = this.highlight(view.state.field(searchState));
- }
- update(update) {
- let state = update.state.field(searchState);
- if (state != update.startState.field(searchState) || update.docChanged || update.selectionSet || update.viewportChanged)
- this.decorations = this.highlight(state);
- }
- highlight({ query, panel }) {
- if (!panel || !query.spec.valid)
- return Decoration.none;
- let { view } = this;
- let builder = new RangeSetBuilder();
- for (let i = 0, ranges = view.visibleRanges, l = ranges.length; i < l; i++) {
- let { from, to } = ranges[i];
- while (i < l - 1 && to > ranges[i + 1].from - 2 * 250 /* HighlightMargin */)
- to = ranges[++i].to;
- query.highlight(view.state.doc, from, to, (from, to) => {
- let selected = view.state.selection.ranges.some(r => r.from == from && r.to == to);
- builder.add(from, to, selected ? selectedMatchMark : matchMark);
- });
- }
- return builder.finish();
- }
- }, {
- decorations: v => v.decorations
- });
- function searchCommand(f) {
- return view => {
- let state = view.state.field(searchState, false);
- return state && state.query.spec.valid ? f(view, state) : openSearchPanel(view);
- };
- }
- /**
- Open the search panel if it isn't already open, and move the
- selection to the first match after the current main selection.
- Will wrap around to the start of the document when it reaches the
- end.
- */
- const findNext = /*@__PURE__*/searchCommand((view, { query }) => {
- let { from, to } = view.state.selection.main;
- let next = query.nextMatch(view.state.doc, from, to);
- if (!next || next.from == from && next.to == to)
- return false;
- view.dispatch({
- selection: { anchor: next.from, head: next.to },
- scrollIntoView: true,
- effects: announceMatch(view, next),
- userEvent: "select.search"
- });
- return true;
- });
- /**
- Move the selection to the previous instance of the search query,
- before the current main selection. Will wrap past the start
- of the document to start searching at the end again.
- */
- const findPrevious = /*@__PURE__*/searchCommand((view, { query }) => {
- let { state } = view, { from, to } = state.selection.main;
- let range = query.prevMatch(state.doc, from, to);
- if (!range)
- return false;
- view.dispatch({
- selection: { anchor: range.from, head: range.to },
- scrollIntoView: true,
- effects: announceMatch(view, range),
- userEvent: "select.search"
- });
- return true;
- });
- /**
- Select all instances of the search query.
- */
- const selectMatches = /*@__PURE__*/searchCommand((view, { query }) => {
- let ranges = query.matchAll(view.state.doc, 1000);
- if (!ranges || !ranges.length)
- return false;
- view.dispatch({
- selection: EditorSelection.create(ranges.map(r => EditorSelection.range(r.from, r.to))),
- userEvent: "select.search.matches"
- });
- return true;
- });
- /**
- Select all instances of the currently selected text.
- */
- const selectSelectionMatches = ({ state, dispatch }) => {
- let sel = state.selection;
- if (sel.ranges.length > 1 || sel.main.empty)
- return false;
- let { from, to } = sel.main;
- let ranges = [], main = 0;
- for (let cur = new SearchCursor(state.doc, state.sliceDoc(from, to)); !cur.next().done;) {
- if (ranges.length > 1000)
- return false;
- if (cur.value.from == from)
- main = ranges.length;
- ranges.push(EditorSelection.range(cur.value.from, cur.value.to));
- }
- dispatch(state.update({
- selection: EditorSelection.create(ranges, main),
- userEvent: "select.search.matches"
- }));
- return true;
- };
- /**
- Replace the current match of the search query.
- */
- const replaceNext = /*@__PURE__*/searchCommand((view, { query }) => {
- let { state } = view, { from, to } = state.selection.main;
- if (state.readOnly)
- return false;
- let next = query.nextMatch(state.doc, from, from);
- if (!next)
- return false;
- let changes = [], selection, replacement;
- let announce = [];
- if (next.from == from && next.to == to) {
- replacement = state.toText(query.getReplacement(next));
- changes.push({ from: next.from, to: next.to, insert: replacement });
- next = query.nextMatch(state.doc, next.from, next.to);
- announce.push(EditorView.announce.of(state.phrase("replaced match on line $", state.doc.lineAt(from).number) + "."));
- }
- if (next) {
- let off = changes.length == 0 || changes[0].from >= next.to ? 0 : next.to - next.from - replacement.length;
- selection = { anchor: next.from - off, head: next.to - off };
- announce.push(announceMatch(view, next));
- }
- view.dispatch({
- changes, selection,
- scrollIntoView: !!selection,
- effects: announce,
- userEvent: "input.replace"
- });
- return true;
- });
- /**
- Replace all instances of the search query with the given
- replacement.
- */
- const replaceAll = /*@__PURE__*/searchCommand((view, { query }) => {
- if (view.state.readOnly)
- return false;
- let changes = query.matchAll(view.state.doc, 1e9).map(match => {
- let { from, to } = match;
- return { from, to, insert: query.getReplacement(match) };
- });
- if (!changes.length)
- return false;
- let announceText = view.state.phrase("replaced $ matches", changes.length) + ".";
- view.dispatch({
- changes,
- effects: EditorView.announce.of(announceText),
- userEvent: "input.replace.all"
- });
- return true;
- });
- function createSearchPanel(view) {
- return view.state.facet(searchConfigFacet).createPanel(view);
- }
- function defaultQuery(state, fallback) {
- var _a;
- let sel = state.selection.main;
- let selText = sel.empty || sel.to > sel.from + 100 ? "" : state.sliceDoc(sel.from, sel.to);
- let caseSensitive = (_a = fallback === null || fallback === void 0 ? void 0 : fallback.caseSensitive) !== null && _a !== void 0 ? _a : state.facet(searchConfigFacet).caseSensitive;
- return fallback && !selText ? fallback : new SearchQuery({ search: selText.replace(/\n/g, "\\n"), caseSensitive });
- }
- /**
- Make sure the search panel is open and focused.
- */
- const openSearchPanel = view => {
- let state = view.state.field(searchState, false);
- if (state && state.panel) {
- let panel = getPanel(view, createSearchPanel);
- if (!panel)
- return false;
- let searchInput = panel.dom.querySelector("[main-field]");
- if (searchInput && searchInput != view.root.activeElement) {
- let query = defaultQuery(view.state, state.query.spec);
- if (query.valid)
- view.dispatch({ effects: setSearchQuery.of(query) });
- searchInput.focus();
- searchInput.select();
- }
- }
- else {
- view.dispatch({ effects: [
- togglePanel.of(true),
- state ? setSearchQuery.of(defaultQuery(view.state, state.query.spec)) : StateEffect.appendConfig.of(searchExtensions)
- ] });
- }
- return true;
- };
- /**
- Close the search panel.
- */
- const closeSearchPanel = view => {
- let state = view.state.field(searchState, false);
- if (!state || !state.panel)
- return false;
- let panel = getPanel(view, createSearchPanel);
- if (panel && panel.dom.contains(view.root.activeElement))
- view.focus();
- view.dispatch({ effects: togglePanel.of(false) });
- return true;
- };
- /**
- Default search-related key bindings.
- - Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel)
- - F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext)
- - Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious)
- - Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine)
- - Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence)
- */
- const searchKeymap = [
- { key: "Mod-f", run: openSearchPanel, scope: "editor search-panel" },
- { key: "F3", run: findNext, shift: findPrevious, scope: "editor search-panel", preventDefault: true },
- { key: "Mod-g", run: findNext, shift: findPrevious, scope: "editor search-panel", preventDefault: true },
- { key: "Escape", run: closeSearchPanel, scope: "editor search-panel" },
- { key: "Mod-Shift-l", run: selectSelectionMatches },
- { key: "Alt-g", run: gotoLine },
- { key: "Mod-d", run: selectNextOccurrence, preventDefault: true },
- ];
- class SearchPanel {
- constructor(view) {
- this.view = view;
- let query = this.query = view.state.field(searchState).query.spec;
- this.commit = this.commit.bind(this);
- this.searchField = elt("input", {
- value: query.search,
- placeholder: phrase(view, "Find"),
- "aria-label": phrase(view, "Find"),
- class: "cm-textfield",
- name: "search",
- "main-field": "true",
- onchange: this.commit,
- onkeyup: this.commit
- });
- this.replaceField = elt("input", {
- value: query.replace,
- placeholder: phrase(view, "Replace"),
- "aria-label": phrase(view, "Replace"),
- class: "cm-textfield",
- name: "replace",
- onchange: this.commit,
- onkeyup: this.commit
- });
- this.caseField = elt("input", {
- type: "checkbox",
- name: "case",
- checked: query.caseSensitive,
- onchange: this.commit
- });
- this.reField = elt("input", {
- type: "checkbox",
- name: "re",
- checked: query.regexp,
- onchange: this.commit
- });
- function button(name, onclick, content) {
- return elt("button", { class: "cm-button", name, onclick, type: "button" }, content);
- }
- this.dom = elt("div", { onkeydown: (e) => this.keydown(e), class: "cm-search" }, [
- this.searchField,
- button("next", () => findNext(view), [phrase(view, "next")]),
- button("prev", () => findPrevious(view), [phrase(view, "previous")]),
- button("select", () => selectMatches(view), [phrase(view, "all")]),
- elt("label", null, [this.caseField, phrase(view, "match case")]),
- elt("label", null, [this.reField, phrase(view, "regexp")]),
- ...view.state.readOnly ? [] : [
- elt("br"),
- this.replaceField,
- button("replace", () => replaceNext(view), [phrase(view, "replace")]),
- button("replaceAll", () => replaceAll(view), [phrase(view, "replace all")]),
- elt("button", {
- name: "close",
- onclick: () => closeSearchPanel(view),
- "aria-label": phrase(view, "close"),
- type: "button"
- }, ["×"])
- ]
- ]);
- }
- commit() {
- let query = new SearchQuery({
- search: this.searchField.value,
- caseSensitive: this.caseField.checked,
- regexp: this.reField.checked,
- replace: this.replaceField.value
- });
- if (!query.eq(this.query)) {
- this.query = query;
- this.view.dispatch({ effects: setSearchQuery.of(query) });
- }
- }
- keydown(e) {
- if (runScopeHandlers(this.view, e, "search-panel")) {
- e.preventDefault();
- }
- else if (e.keyCode == 13 && e.target == this.searchField) {
- e.preventDefault();
- (e.shiftKey ? findPrevious : findNext)(this.view);
- }
- else if (e.keyCode == 13 && e.target == this.replaceField) {
- e.preventDefault();
- replaceNext(this.view);
- }
- }
- update(update) {
- for (let tr of update.transactions)
- for (let effect of tr.effects) {
- if (effect.is(setSearchQuery) && !effect.value.eq(this.query))
- this.setQuery(effect.value);
- }
- }
- setQuery(query) {
- this.query = query;
- this.searchField.value = query.search;
- this.replaceField.value = query.replace;
- this.caseField.checked = query.caseSensitive;
- this.reField.checked = query.regexp;
- }
- mount() {
- this.searchField.select();
- }
- get pos() { return 80; }
- get top() { return this.view.state.facet(searchConfigFacet).top; }
- }
- function phrase(view, phrase) { return view.state.phrase(phrase); }
- const AnnounceMargin = 30;
- const Break = /[\s\.,:;?!]/;
- function announceMatch(view, { from, to }) {
- let line = view.state.doc.lineAt(from), lineEnd = view.state.doc.lineAt(to).to;
- let start = Math.max(line.from, from - AnnounceMargin), end = Math.min(lineEnd, to + AnnounceMargin);
- let text = view.state.sliceDoc(start, end);
- if (start != line.from) {
- for (let i = 0; i < AnnounceMargin; i++)
- if (!Break.test(text[i + 1]) && Break.test(text[i])) {
- text = text.slice(i);
- break;
- }
- }
- if (end != lineEnd) {
- for (let i = text.length - 1; i > text.length - AnnounceMargin; i--)
- if (!Break.test(text[i - 1]) && Break.test(text[i])) {
- text = text.slice(0, i);
- break;
- }
- }
- return EditorView.announce.of(`${view.state.phrase("current match")}. ${text} ${view.state.phrase("on line")} ${line.number}.`);
- }
- const baseTheme = /*@__PURE__*/EditorView.baseTheme({
- ".cm-panel.cm-search": {
- padding: "2px 6px 4px",
- position: "relative",
- "& [name=close]": {
- position: "absolute",
- top: "0",
- right: "4px",
- backgroundColor: "inherit",
- border: "none",
- font: "inherit",
- padding: 0,
- margin: 0
- },
- "& input, & button, & label": {
- margin: ".2em .6em .2em 0"
- },
- "& input[type=checkbox]": {
- marginRight: ".2em"
- },
- "& label": {
- fontSize: "80%",
- whiteSpace: "pre"
- }
- },
- "&light .cm-searchMatch": { backgroundColor: "#ffff0054" },
- "&dark .cm-searchMatch": { backgroundColor: "#00ffff8a" },
- "&light .cm-searchMatch-selected": { backgroundColor: "#ff6a0054" },
- "&dark .cm-searchMatch-selected": { backgroundColor: "#ff00ff8a" }
- });
- const searchExtensions = [
- searchState,
- /*@__PURE__*/Prec.lowest(searchHighlighter),
- baseTheme
- ];
- export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
|