diff --git a/packages/codemirror/src/find_replace.ts b/packages/codemirror/src/find_replace.ts index c0687493f..53bbe5b9b 100644 --- a/packages/codemirror/src/find_replace.ts +++ b/packages/codemirror/src/find_replace.ts @@ -1,8 +1,13 @@ import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view"; -import { StateEffect, Compartment } from "@codemirror/state"; +import { StateEffect, Compartment, EditorSelection, RangeSet } from "@codemirror/state"; const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" }); +interface Match { + from: number; + to: number; +} + export function createSearchHighlighter(view: EditorView, searchTerm: string, matchCase: boolean, wholeWord: boolean) { // Escape the search term for use in RegExp const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -16,17 +21,49 @@ export function createSearchHighlighter(view: EditorView, searchTerm: string, ma }); return ViewPlugin.fromClass(class SearchHighlighter { - matches = matcher.createDeco(view); - totalFound = this.matches.size; + matches!: RangeSet; + totalFound: number; + private parsedMatches: Match[]; - constructor(public view: EditorView) { } + constructor(public view: EditorView) { + this.parsedMatches = []; + this.totalFound = 0; + this.updateSearchData(view); + } + + updateSearchData(view: EditorView) { + const matches = matcher.createDeco(view); + const cursor = matches.iter(); + while (cursor.value) { + this.parsedMatches.push({ + from: cursor.from, + to: cursor.to + }); + cursor.next(); + } + + this.matches = matches; + this.totalFound = this.parsedMatches.length; + } update(update: ViewUpdate) { if (update.docChanged || update.viewportChanged) { - this.matches = matcher.createDeco(update.view); + this.updateSearchData(update.view); } } + scrollToMatch(matchIndex: number) { + if (this.parsedMatches.length <= matchIndex) { + return; + } + + const pos = this.parsedMatches[matchIndex]; + this.view.dispatch({ + effects: EditorView.scrollIntoView(pos.from, { y: "center" }), + scrollIntoView: true + }); + } + destroy() { // Do nothing. } diff --git a/packages/codemirror/src/index.ts b/packages/codemirror/src/index.ts index b22d4ecc1..6f7f1be9b 100644 --- a/packages/codemirror/src/index.ts +++ b/packages/codemirror/src/index.ts @@ -176,9 +176,13 @@ export default class CodeMirror extends EditorView { this.dispatch({ effects: this.searchHighlightCompartment.reconfigure(plugin) }); + // Wait for the plugin to activate in the next render cycle await new Promise(requestAnimationFrame); - const instance = this.plugin(plugin); // TS workaround + const instance = this.plugin(plugin); + if (instance) { + instance.scrollToMatch(0); + } return { totalFound: instance?.totalFound ?? 0