@codemirror/search
Advanced tools
Comparing version 0.18.1 to 0.18.2
@@ -0,1 +1,17 @@ | ||
## 0.18.2 (2021-03-19) | ||
### Bug fixes | ||
The search interface and cursor will no longer include overlapping matches (aligning with what all other editors are doing). | ||
### New features | ||
The package now exports a `RegExpCursor` which is a search cursor that matches regular expression patterns. | ||
The search/replace interface now allows the user to use regular expressions. | ||
The `SearchCursor` class now has a `nextOverlapping` method that includes matches that start inside the previous match. | ||
Basic backslash escapes (\n, \r, \t, and \\) are now accepted in string search patterns in the UI. | ||
## 0.18.1 (2021-03-15) | ||
@@ -2,0 +18,0 @@ |
@@ -55,5 +55,36 @@ import { Command, KeyBinding } from '@codemirror/view'; | ||
next(): this; | ||
/** | ||
The `next` method will ignore matches that partially overlap a | ||
previous match. This method behaves like `next`, but includes | ||
such matches. | ||
*/ | ||
nextOverlapping(): this; | ||
private match; | ||
} | ||
declare class RegExpCursor implements Iterator<{ | ||
from: number; | ||
to: number; | ||
match: RegExpExecArray; | ||
}> { | ||
private to; | ||
private iter; | ||
private re; | ||
private curLine; | ||
private curLineStart; | ||
private matchPos; | ||
done: boolean; | ||
value: { | ||
from: number; | ||
to: number; | ||
match: RegExpExecArray; | ||
}; | ||
constructor(text: Text, query: string, options?: { | ||
ignoreCase?: boolean; | ||
}, from?: number, to?: number); | ||
private getLine; | ||
private nextLine; | ||
next(): this; | ||
} | ||
/** | ||
@@ -146,2 +177,2 @@ Command that shows a dialog asking the user for a line number, and | ||
export { SearchCursor, closeSearchPanel, findNext, findPrevious, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, searchKeymap, selectMatches, selectSelectionMatches }; | ||
export { RegExpCursor, SearchCursor, closeSearchPanel, findNext, findPrevious, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, searchKeymap, selectMatches, selectSelectionMatches }; |
@@ -6,5 +6,6 @@ import { EditorView, Decoration, ViewPlugin, runScopeHandlers } from '@codemirror/view'; | ||
import elt from 'crelt'; | ||
import { findClusterBreak } from '@codemirror/text'; | ||
import { codePointAt, fromCodePoint, codePointSize, findClusterBreak } from '@codemirror/text'; | ||
const basicNormalize = typeof String.prototype.normalize == "function" ? x => x.normalize("NFKD") : x => x; | ||
const basicNormalize = typeof String.prototype.normalize == "function" | ||
? x => x.normalize("NFKD") : x => x; | ||
/** | ||
@@ -56,3 +57,3 @@ A search cursor provides an iterator over text matches in a | ||
} | ||
return this.buffer.charCodeAt(this.bufferPos); | ||
return codePointAt(this.buffer, this.bufferPos); | ||
} | ||
@@ -66,2 +67,12 @@ /** | ||
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 (;;) { | ||
@@ -73,11 +84,4 @@ let next = this.peek(); | ||
} | ||
let str = String.fromCharCode(next), start = this.bufferStart + this.bufferPos; | ||
this.bufferPos++; | ||
for (;;) { | ||
let peek = this.peek(); | ||
if (peek < 0xDC00 || peek >= 0xE000) | ||
break; | ||
this.bufferPos++; | ||
str += String.fromCharCode(peek); | ||
} | ||
let str = fromCodePoint(next), start = this.bufferStart + this.bufferPos; | ||
this.bufferPos += codePointSize(next); | ||
let norm = this.normalize(str); | ||
@@ -126,2 +130,145 @@ for (let i = 0, pos = start;; i++) { | ||
const empty = { from: -1, to: -1, match: /.*/.exec("") }; | ||
const baseFlags = "gm" + (/x/.unicode == null ? "" : "u"); | ||
class RegExpCursor { | ||
constructor(text, query, options, from = 0, to = text.length) { | ||
this.to = to; | ||
this.curLine = ""; | ||
this.done = false; | ||
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); | ||
} | ||
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)); | ||
} | ||
} | ||
} | ||
} | ||
function validRegExp(source) { | ||
try { | ||
new RegExp(source, baseFlags); | ||
return true; | ||
} | ||
catch (_a) { | ||
return false; | ||
} | ||
} | ||
function createLineDialog(view) { | ||
@@ -146,3 +293,3 @@ let input = elt("input", { class: "cm-textfield", name: "line" }); | ||
} | ||
}, elt("label", view.state.phrase("Go to line:"), " ", input), " ", elt("button", { class: "cm-button", type: "submit" }, view.state.phrase("go"))); | ||
}, elt("label", view.state.phrase("Go to line"), ": ", input), " ", elt("button", { class: "cm-button", type: "submit" }, view.state.phrase("go"))); | ||
function go() { | ||
@@ -296,3 +443,3 @@ let match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(input.value); | ||
let cursor = new SearchCursor(state.doc, query, part.from, part.to); | ||
while (!cursor.next().done) { | ||
while (!cursor.nextOverlapping().done) { | ||
let { from, to } = cursor.value; | ||
@@ -327,9 +474,107 @@ if (!check || ((from == 0 || check(state.sliceDoc(from - 1, from)) != CharCategory.Word) && | ||
eq(other) { | ||
return this.search == other.search && this.replace == other.replace && this.caseInsensitive == other.caseInsensitive; | ||
return this.search == other.search && this.replace == other.replace && | ||
this.caseInsensitive == other.caseInsensitive && this.constructor == other.constructor; | ||
} | ||
} | ||
class StringQuery extends Query { | ||
constructor(search, replace, caseInsensitive) { | ||
super(search, replace, caseInsensitive); | ||
this.unquoted = search.replace(/\\([nrt\\])/g, (_, ch) => ch == "n" ? "\n" : ch == "r" ? "\r" : ch == "t" ? "\t" : "\\"); | ||
} | ||
cursor(doc, from = 0, to = doc.length) { | ||
return new SearchCursor(doc, this.search, from, to, this.caseInsensitive ? x => x.toLowerCase() : undefined); | ||
return new SearchCursor(doc, this.unquoted, from, to, this.caseInsensitive ? x => x.toLowerCase() : undefined); | ||
} | ||
nextMatch(doc, curFrom, curTo) { | ||
let cursor = this.cursor(doc, curTo).nextOverlapping(); | ||
if (cursor.done) | ||
cursor = this.cursor(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.unquoted.length); | ||
let cursor = this.cursor(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.replace; } | ||
matchAll(doc, limit) { | ||
let cursor = this.cursor(doc), ranges = []; | ||
while (!cursor.next().done) { | ||
if (ranges.length >= limit) | ||
return null; | ||
ranges.push(cursor.value); | ||
} | ||
return ranges; | ||
} | ||
highlight(doc, from, to, add) { | ||
let cursor = this.cursor(doc, Math.max(0, from - this.unquoted.length), Math.min(to + this.unquoted.length, doc.length)); | ||
while (!cursor.next().done) | ||
add(cursor.value.from, cursor.value.to); | ||
} | ||
get valid() { return !!this.search; } | ||
} | ||
class RegExpQuery extends Query { | ||
constructor(search, replace, caseInsensitive) { | ||
super(search, replace, caseInsensitive); | ||
this.valid = !!search && validRegExp(search); | ||
} | ||
cursor(doc, from = 0, to = doc.length) { | ||
return new RegExpCursor(doc, this.search, this.caseInsensitive ? { ignoreCase: true } : undefined, from, to); | ||
} | ||
nextMatch(doc, curFrom, curTo) { | ||
let cursor = this.cursor(doc, curTo).next(); | ||
if (cursor.done) | ||
cursor = this.cursor(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 = this.cursor(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.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 = this.cursor(doc), ranges = []; | ||
while (!cursor.next().done) { | ||
if (ranges.length >= limit) | ||
return null; | ||
ranges.push(cursor.value); | ||
} | ||
return ranges; | ||
} | ||
highlight(doc, from, to, add) { | ||
let cursor = this.cursor(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); | ||
} | ||
} | ||
const setQuery = StateEffect.define(); | ||
@@ -339,3 +584,3 @@ const togglePanel = StateEffect.define(); | ||
create() { | ||
return new SearchState(new Query("", "", false), null); | ||
return new SearchState(new StringQuery("", "", false), null); | ||
}, | ||
@@ -373,9 +618,12 @@ update(value, tr) { | ||
return Decoration.none; | ||
let state = this.view.state, viewport = this.view.viewport; | ||
let cursor = query.cursor(state.doc, Math.max(0, viewport.from - query.search.length), Math.min(viewport.to + query.search.length, state.doc.length)); | ||
let { view } = this; | ||
let builder = new RangeSetBuilder(); | ||
while (!cursor.next().done) { | ||
let { from, to } = cursor.value; | ||
let selected = state.selection.ranges.some(r => r.from == from && r.to == to); | ||
builder.add(from, to, selected ? selectedMatchMark : matchMark); | ||
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); | ||
}); | ||
} | ||
@@ -393,11 +641,2 @@ return builder.finish(); | ||
} | ||
function findNextMatch(doc, from, query) { | ||
let cursor = query.cursor(doc, from).next(); | ||
if (cursor.done) { | ||
cursor = query.cursor(doc, 0, from + query.search.length - 1).next(); | ||
if (cursor.done) | ||
return null; | ||
} | ||
return cursor.value; | ||
} | ||
/** | ||
@@ -409,5 +648,5 @@ Open the search panel if it isn't already open, and move the | ||
*/ | ||
const findNext = searchCommand((view, state) => { | ||
const findNext = searchCommand((view, { query }) => { | ||
let { from, to } = view.state.selection.main; | ||
let next = findNextMatch(view.state.doc, view.state.selection.main.from + 1, state.query); | ||
let next = query.nextMatch(view.state.doc, from, to); | ||
if (!next || next.from == from && next.to == to) | ||
@@ -422,18 +661,2 @@ return false; | ||
}); | ||
const FindPrevChunkSize = 10000; | ||
// Searching in reverse is, rather than implementing inverted search | ||
// cursor, done by scanning chunk after chunk forward. | ||
function findPrevInRange(query, doc, from, to) { | ||
for (let pos = to;;) { | ||
let start = Math.max(from, pos - FindPrevChunkSize - query.search.length); | ||
let cursor = query.cursor(doc, start, pos), range = null; | ||
while (!cursor.next().done) | ||
range = cursor.value; | ||
if (range) | ||
return range; | ||
if (start == from) | ||
return null; | ||
pos -= FindPrevChunkSize; | ||
} | ||
} | ||
/** | ||
@@ -445,5 +668,4 @@ Move the selection to the previous instance of the search query, | ||
const findPrevious = searchCommand((view, { query }) => { | ||
let { state } = view; | ||
let range = findPrevInRange(query, state.doc, 0, state.selection.main.to - 1) || | ||
findPrevInRange(query, state.doc, state.selection.main.from + 1, state.doc.length); | ||
let { state } = view, { from, to } = state.selection.main; | ||
let range = query.prevMatch(state.doc, from, to); | ||
if (!range) | ||
@@ -462,8 +684,8 @@ return false; | ||
const selectMatches = searchCommand((view, { query }) => { | ||
let cursor = query.cursor(view.state.doc), ranges = []; | ||
while (!cursor.next().done) | ||
ranges.push(EditorSelection.range(cursor.value.from, cursor.value.to)); | ||
if (!ranges.length) | ||
let ranges = query.matchAll(view.state.doc, 1000); | ||
if (!ranges || !ranges.length) | ||
return false; | ||
view.dispatch({ selection: EditorSelection.create(ranges) }); | ||
view.dispatch({ | ||
selection: EditorSelection.create(ranges.map(r => EditorSelection.range(r.from, r.to))) | ||
}); | ||
return true; | ||
@@ -494,12 +716,14 @@ }); | ||
const replaceNext = searchCommand((view, { query }) => { | ||
let { state } = view, next = findNextMatch(state.doc, state.selection.main.from, query); | ||
let { state } = view, { from, to } = state.selection.main; | ||
let next = query.nextMatch(state.doc, from, from); | ||
if (!next) | ||
return false; | ||
let { from, to } = state.selection.main, changes = [], selection; | ||
let changes = [], selection, replacement; | ||
if (next.from == from && next.to == to) { | ||
changes.push({ from: next.from, to: next.to, insert: query.replace }); | ||
next = findNextMatch(state.doc, next.to, query); | ||
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); | ||
} | ||
if (next) { | ||
let off = changes.length == 0 || changes[0].from >= next.to ? 0 : next.to - next.from - query.replace.length; | ||
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 }; | ||
@@ -519,7 +743,6 @@ } | ||
const replaceAll = searchCommand((view, { query }) => { | ||
let cursor = query.cursor(view.state.doc), changes = []; | ||
while (!cursor.next().done) { | ||
let { from, to } = cursor.value; | ||
changes.push({ from, to, insert: query.replace }); | ||
} | ||
let changes = query.matchAll(view.state.doc, 1e9).map(match => { | ||
let { from, to } = match; | ||
return { from, to, insert: query.getReplacement(match) }; | ||
}); | ||
if (!changes.length) | ||
@@ -595,7 +818,7 @@ return false; | ||
function buildPanel(conf) { | ||
function p(phrase) { return conf.view.state.phrase(phrase); } | ||
function phrase(phrase) { return conf.view.state.phrase(phrase); } | ||
let searchField = elt("input", { | ||
value: conf.query.search, | ||
placeholder: p("Find"), | ||
"aria-label": p("Find"), | ||
placeholder: phrase("Find"), | ||
"aria-label": phrase("Find"), | ||
class: "cm-textfield", | ||
@@ -608,4 +831,4 @@ name: "search", | ||
value: conf.query.replace, | ||
placeholder: p("Replace"), | ||
"aria-label": p("Replace"), | ||
placeholder: phrase("Replace"), | ||
"aria-label": phrase("Replace"), | ||
class: "cm-textfield", | ||
@@ -622,4 +845,10 @@ name: "replace", | ||
}); | ||
let reField = elt("input", { | ||
type: "checkbox", | ||
name: "re", | ||
checked: conf.query instanceof RegExpQuery, | ||
onchange: update | ||
}); | ||
function update() { | ||
conf.updateQuery(new Query(searchField.value, replaceField.value, !caseField.checked)); | ||
conf.updateQuery(new (reField.checked ? RegExpQuery : StringQuery)(searchField.value, replaceField.value, !caseField.checked)); | ||
} | ||
@@ -644,11 +873,12 @@ function keydown(e) { | ||
searchField, | ||
button("next", () => findNext(conf.view), [p("next")]), | ||
button("prev", () => findPrevious(conf.view), [p("previous")]), | ||
button("select", () => selectMatches(conf.view), [p("all")]), | ||
elt("label", null, [caseField, "match case"]), | ||
button("next", () => findNext(conf.view), [phrase("next")]), | ||
button("prev", () => findPrevious(conf.view), [phrase("previous")]), | ||
button("select", () => selectMatches(conf.view), [phrase("all")]), | ||
elt("label", null, [caseField, phrase("match case")]), | ||
elt("label", null, [reField, phrase("regexp")]), | ||
elt("br"), | ||
replaceField, | ||
button("replace", () => replaceNext(conf.view), [p("replace")]), | ||
button("replaceAll", () => replaceAll(conf.view), [p("replace all")]), | ||
elt("button", { name: "close", onclick: () => closeSearchPanel(conf.view), "aria-label": p("close") }, ["×"]) | ||
button("replace", () => replaceNext(conf.view), [phrase("replace")]), | ||
button("replaceAll", () => replaceAll(conf.view), [phrase("replace all")]), | ||
elt("button", { name: "close", onclick: () => closeSearchPanel(conf.view), "aria-label": phrase("close") }, ["×"]) | ||
]); | ||
@@ -660,4 +890,2 @@ return panel; | ||
function announceMatch(view, { from, to }) { | ||
if (view.hasFocus) | ||
return undefined; | ||
let lineStart = view.state.doc.lineAt(from).from, lineEnd = view.state.doc.lineAt(to).to; | ||
@@ -696,5 +924,8 @@ let start = Math.max(lineStart, from - AnnounceMargin), end = Math.min(lineEnd, to + AnnounceMargin); | ||
}, | ||
"& input, & button": { | ||
margin: ".2em .5em .2em 0" | ||
"& input, & button, & label": { | ||
margin: ".2em .6em .2em 0" | ||
}, | ||
"& input[type=checkbox]": { | ||
marginRight: ".2em" | ||
}, | ||
"& label": { | ||
@@ -715,2 +946,2 @@ fontSize: "80%" | ||
export { SearchCursor, closeSearchPanel, findNext, findPrevious, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, searchKeymap, selectMatches, selectSelectionMatches }; | ||
export { RegExpCursor, SearchCursor, closeSearchPanel, findNext, findPrevious, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, searchKeymap, selectMatches, selectSelectionMatches }; |
{ | ||
"name": "@codemirror/search", | ||
"version": "0.18.1", | ||
"version": "0.18.2", | ||
"description": "Search functionality for the CodeMirror code editor", | ||
@@ -5,0 +5,0 @@ "scripts": { |
Sorry, the diff of this file is not supported yet
79849
2005