1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150 |
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- var view = require('@codemirror/view');
- var state = require('@codemirror/state');
- var elt = require('crelt');
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
- var elt__default = /*#__PURE__*/_interopDefaultLegacy(elt);
- 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 state.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 = state.fromCodePoint(next), start = this.bufferStart + this.bufferPos;
- this.bufferPos += state.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: /.*/.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 = 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__default["default"]("input", { class: "cm-textfield", name: "line" });
- let dom = elt__default["default"]("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__default["default"]("label", view.state.phrase("Go to line"), ": ", input), " ", elt__default["default"]("button", { class: "cm-button", type: "submit" }, view.state.phrase("go")));
- function go() {
- let match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(input.value);
- if (!match)
- return;
- let { state: state$1 } = view, startLine = state$1.doc.lineAt(state$1.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$1.doc.lines);
- line = Math.round(state$1.doc.lines * pc);
- }
- else if (ln && sign) {
- line = line * (sign == "-" ? -1 : 1) + startLine.number;
- }
- let docLine = state$1.doc.line(Math.max(1, Math.min(state$1.doc.lines, line)));
- view.dispatch({
- effects: dialogEffect.of(false),
- selection: state.EditorSelection.cursor(docLine.from + Math.max(0, Math.min(col, docLine.length))),
- scrollIntoView: true
- });
- view.focus();
- }
- return { dom };
- }
- const dialogEffect = state.StateEffect.define();
- const dialogField = state.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 => view.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$1 => {
- let panel = view.getPanel(view$1, createLineDialog);
- if (!panel) {
- let effects = [dialogEffect.of(true)];
- if (view$1.state.field(dialogField, false) == null)
- effects.push(state.StateEffect.appendConfig.of([dialogField, baseTheme$1]));
- view$1.dispatch({ effects });
- panel = view.getPanel(view$1, createLineDialog);
- }
- if (panel)
- panel.dom.querySelector("input").focus();
- return true;
- };
- const baseTheme$1 = view.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 = state.Facet.define({
- combine(options) {
- return state.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 = view.Decoration.mark({ class: "cm-selectionMatch" });
- const mainMatchDeco = view.Decoration.mark({ class: "cm-selectionMatch cm-selectionMatch-main" });
- // Whether the characters directly outside the given positions are non-word characters
- function insideWordBoundaries(check, state$1, from, to) {
- return (from == 0 || check(state$1.sliceDoc(from - 1, from)) != state.CharCategory.Word) &&
- (to == state$1.doc.length || check(state$1.sliceDoc(to, to + 1)) != state.CharCategory.Word);
- }
- // Whether the characters directly at the given positions are word characters
- function insideWord(check, state$1, from, to) {
- return check(state$1.sliceDoc(from, from + 1)) == state.CharCategory.Word
- && check(state$1.sliceDoc(to - 1, to)) == state.CharCategory.Word;
- }
- const matchHighlighter = view.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$1) {
- let conf = view$1.state.facet(highlightConfig);
- let { state } = view$1, sel = state.selection;
- if (sel.ranges.length > 1)
- return view.Decoration.none;
- let range = sel.main, query, check = null;
- if (range.empty) {
- if (!conf.highlightWordAroundCursor)
- return view.Decoration.none;
- let word = state.wordAt(range.head);
- if (!word)
- return view.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 view.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 view.Decoration.none;
- }
- else {
- query = state.sliceDoc(range.from, range.to).trim();
- if (!query)
- return view.Decoration.none;
- }
- }
- let deco = [];
- for (let part of view$1.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 view.Decoration.none;
- }
- }
- }
- return view.Decoration.set(deco);
- }
- }, {
- decorations: v => v.decorations
- });
- const defaultTheme = view.EditorView.baseTheme({
- ".cm-selectionMatch": { backgroundColor: "#99ff7780" },
- ".cm-searchMatch .cm-selectionMatch": { backgroundColor: "transparent" }
- });
- // Select the words around the cursors.
- const selectWord = ({ state: state$1, dispatch }) => {
- let { selection } = state$1;
- let newSel = state.EditorSelection.create(selection.ranges.map(range => state$1.wordAt(range.head) || state.EditorSelection.cursor(range.head)), selection.mainIndex);
- if (newSel.eq(selection))
- return false;
- dispatch(state$1.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: state$1, dispatch }) => {
- let { ranges } = state$1.selection;
- if (ranges.some(sel => sel.from === sel.to))
- return selectWord({ state: state$1, dispatch });
- let searchedText = state$1.sliceDoc(ranges[0].from, ranges[0].to);
- if (state$1.selection.ranges.some(r => state$1.sliceDoc(r.from, r.to) != searchedText))
- return false;
- let range = findNextOccurrence(state$1, searchedText);
- if (!range)
- return false;
- dispatch(state$1.update({
- selection: state$1.selection.addRange(state.EditorSelection.range(range.from, range.to), false),
- effects: view.EditorView.scrollIntoView(range.to)
- }));
- return true;
- };
- const searchConfigFacet = state.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 = state.StateEffect.define();
- const togglePanel = state.StateEffect.define();
- const searchState = state.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 => view.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 = view.Decoration.mark({ class: "cm-searchMatch" }), selectedMatchMark = view.Decoration.mark({ class: "cm-searchMatch cm-searchMatch-selected" });
- const searchHighlighter = view.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 view.Decoration.none;
- let { view: view$1 } = this;
- let builder = new state.RangeSetBuilder();
- for (let i = 0, ranges = view$1.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$1.state.doc, from, to, (from, to) => {
- let selected = view$1.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 = 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 = 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 = searchCommand((view, { query }) => {
- let ranges = query.matchAll(view.state.doc, 1000);
- if (!ranges || !ranges.length)
- return false;
- view.dispatch({
- selection: state.EditorSelection.create(ranges.map(r => state.EditorSelection.range(r.from, r.to))),
- userEvent: "select.search.matches"
- });
- return true;
- });
- /**
- Select all instances of the currently selected text.
- */
- const selectSelectionMatches = ({ state: state$1, dispatch }) => {
- let sel = state$1.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$1.doc, state$1.sliceDoc(from, to)); !cur.next().done;) {
- if (ranges.length > 1000)
- return false;
- if (cur.value.from == from)
- main = ranges.length;
- ranges.push(state.EditorSelection.range(cur.value.from, cur.value.to));
- }
- dispatch(state$1.update({
- selection: state.EditorSelection.create(ranges, main),
- userEvent: "select.search.matches"
- }));
- return true;
- };
- /**
- Replace the current match of the search query.
- */
- const replaceNext = searchCommand((view$1, { query }) => {
- let { state } = view$1, { 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(view.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$1, next));
- }
- view$1.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 = searchCommand((view$1, { query }) => {
- if (view$1.state.readOnly)
- return false;
- let changes = query.matchAll(view$1.state.doc, 1e9).map(match => {
- let { from, to } = match;
- return { from, to, insert: query.getReplacement(match) };
- });
- if (!changes.length)
- return false;
- let announceText = view$1.state.phrase("replaced $ matches", changes.length) + ".";
- view$1.dispatch({
- changes,
- effects: view.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$1 => {
- let state$1 = view$1.state.field(searchState, false);
- if (state$1 && state$1.panel) {
- let panel = view.getPanel(view$1, createSearchPanel);
- if (!panel)
- return false;
- let searchInput = panel.dom.querySelector("[main-field]");
- if (searchInput && searchInput != view$1.root.activeElement) {
- let query = defaultQuery(view$1.state, state$1.query.spec);
- if (query.valid)
- view$1.dispatch({ effects: setSearchQuery.of(query) });
- searchInput.focus();
- searchInput.select();
- }
- }
- else {
- view$1.dispatch({ effects: [
- togglePanel.of(true),
- state$1 ? setSearchQuery.of(defaultQuery(view$1.state, state$1.query.spec)) : state.StateEffect.appendConfig.of(searchExtensions)
- ] });
- }
- return true;
- };
- /**
- Close the search panel.
- */
- const closeSearchPanel = view$1 => {
- let state = view$1.state.field(searchState, false);
- if (!state || !state.panel)
- return false;
- let panel = view.getPanel(view$1, createSearchPanel);
- if (panel && panel.dom.contains(view$1.root.activeElement))
- view$1.focus();
- view$1.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__default["default"]("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__default["default"]("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__default["default"]("input", {
- type: "checkbox",
- name: "case",
- checked: query.caseSensitive,
- onchange: this.commit
- });
- this.reField = elt__default["default"]("input", {
- type: "checkbox",
- name: "re",
- checked: query.regexp,
- onchange: this.commit
- });
- function button(name, onclick, content) {
- return elt__default["default"]("button", { class: "cm-button", name, onclick, type: "button" }, content);
- }
- this.dom = elt__default["default"]("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__default["default"]("label", null, [this.caseField, phrase(view, "match case")]),
- elt__default["default"]("label", null, [this.reField, phrase(view, "regexp")]),
- ...view.state.readOnly ? [] : [
- elt__default["default"]("br"),
- this.replaceField,
- button("replace", () => replaceNext(view), [phrase(view, "replace")]),
- button("replaceAll", () => replaceAll(view), [phrase(view, "replace all")]),
- elt__default["default"]("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 (view.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$1, { from, to }) {
- let line = view$1.state.doc.lineAt(from), lineEnd = view$1.state.doc.lineAt(to).to;
- let start = Math.max(line.from, from - AnnounceMargin), end = Math.min(lineEnd, to + AnnounceMargin);
- let text = view$1.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 view.EditorView.announce.of(`${view$1.state.phrase("current match")}. ${text} ${view$1.state.phrase("on line")} ${line.number}.`);
- }
- const baseTheme = view.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,
- state.Prec.lowest(searchHighlighter),
- baseTheme
- ];
- exports.RegExpCursor = RegExpCursor;
- exports.SearchCursor = SearchCursor;
- exports.SearchQuery = SearchQuery;
- exports.closeSearchPanel = closeSearchPanel;
- exports.findNext = findNext;
- exports.findPrevious = findPrevious;
- exports.getSearchQuery = getSearchQuery;
- exports.gotoLine = gotoLine;
- exports.highlightSelectionMatches = highlightSelectionMatches;
- exports.openSearchPanel = openSearchPanel;
- exports.replaceAll = replaceAll;
- exports.replaceNext = replaceNext;
- exports.search = search;
- exports.searchKeymap = searchKeymap;
- exports.selectMatches = selectMatches;
- exports.selectNextOccurrence = selectNextOccurrence;
- exports.selectSelectionMatches = selectSelectionMatches;
- exports.setSearchQuery = setSearchQuery;
|