index.cjs 43 KB

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