refactor(code/find): remove inner class

This commit is contained in:
Elian Doran 2025-05-12 20:59:46 +03:00
parent e5417827f4
commit 8a35e390f2
No known key found for this signature in database
2 changed files with 75 additions and 65 deletions

View File

@ -1,5 +1,5 @@
import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view"; import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { StateEffect, Compartment, EditorSelection, RangeSet } from "@codemirror/state"; import { RangeSet, RangeSetBuilder } from "@codemirror/state";
const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" }); const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" });
@ -8,90 +8,99 @@ interface Match {
to: number; to: number;
} }
export function createSearchHighlighter(view: EditorView, searchTerm: string, matchCase: boolean, wholeWord: boolean) { export class SearchHighlighter {
// Escape the search term for use in RegExp matches: RangeSet<Decoration>;
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); currentFound: number;
const wordBoundary = wholeWord ? "\\b" : ""; totalFound: number;
const flags = matchCase ? "g" : "gi"; matcher?: MatchDecorator;
const regex = new RegExp(`${wordBoundary}${escapedTerm}${wordBoundary}`, flags); private parsedMatches: Match[];
const matcher = new MatchDecorator({ constructor(public view: EditorView) {
regexp: regex, this.parsedMatches = [];
decoration: searchMatchDecoration, this.currentFound = 0;
}); this.totalFound = 0;
this.matches = (new RangeSetBuilder<Decoration>()).finish();
}
return ViewPlugin.fromClass(class SearchHighlighter { searchFor(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
matches!: RangeSet<Decoration>; // Escape the search term for use in RegExp
currentFound: number; const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
totalFound: number; const wordBoundary = wholeWord ? "\\b" : "";
private parsedMatches: Match[]; const flags = matchCase ? "g" : "gi";
const regex = new RegExp(`${wordBoundary}${escapedTerm}${wordBoundary}`, flags);
constructor(public view: EditorView) { this.matcher = new MatchDecorator({
this.parsedMatches = []; regexp: regex,
this.currentFound = 0; decoration: searchMatchDecoration,
this.totalFound = 0; });
this.updateSearchData(view); this.updateSearchData(this.view);
}
updateSearchData(view: EditorView) {
if (!this.matcher) {
return;
} }
updateSearchData(view: EditorView) { const matches = this.matcher.createDeco(view);
const matches = matcher.createDeco(view); const cursor = matches.iter();
const cursor = matches.iter(); while (cursor.value) {
while (cursor.value) { this.parsedMatches.push({
this.parsedMatches.push({ from: cursor.from,
from: cursor.from, to: cursor.to
to: cursor.to });
}); cursor.next();
cursor.next();
}
this.matches = matches;
this.totalFound = this.parsedMatches.length;
} }
update(update: ViewUpdate) { this.matches = matches;
if (update.docChanged || update.viewportChanged) { this.totalFound = this.parsedMatches.length;
this.updateSearchData(update.view); }
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.updateSearchData(update.view);
}
}
scrollToMatch(matchIndex: number) {
if (this.parsedMatches.length <= matchIndex) {
return;
} }
scrollToMatch(matchIndex: number) { const match = this.parsedMatches[matchIndex];
if (this.parsedMatches.length <= matchIndex) { this.currentFound = matchIndex + 1;
this.view.dispatch({
effects: EditorView.scrollIntoView(match.from, { y: "center" }),
scrollIntoView: true
});
}
scrollToMatchNearestSelection() {
const cursorPos = this.view.state.selection.main.head;
let index = 0;
for (const match of this.parsedMatches) {
if (match.from >= cursorPos) {
this.scrollToMatch(index);
return; return;
} }
const match = this.parsedMatches[matchIndex]; index++;
this.currentFound = matchIndex + 1;
this.view.dispatch({
effects: EditorView.scrollIntoView(match.from, { y: "center" }),
scrollIntoView: true
});
} }
}
scrollToMatchNearestSelection() { destroy() {
const cursorPos = this.view.state.selection.main.head; // Do nothing.
let index = 0; }
for (const match of this.parsedMatches) {
if (match.from >= cursorPos) {
this.scrollToMatch(index);
return;
}
index++; static deco = (v: SearchHighlighter) => v.matches;
} }
}
destroy() { export function createSearchHighlighter() {
// Do nothing. return ViewPlugin.fromClass(SearchHighlighter, {
}
static deco = (v: SearchHighlighter) => v.matches;
}, {
decorations: v => v.matches, decorations: v => v.matches,
provide: (plugin) => plugin provide: (plugin) => plugin
}); });
} }
export const searchMatchHighlightTheme = EditorView.baseTheme({ export const searchMatchHighlightTheme = EditorView.baseTheme({
".cm-searchMatch": { ".cm-searchMatch": {
backgroundColor: "rgba(255, 255, 0, 0.4)", backgroundColor: "rgba(255, 255, 0, 0.4)",

View File

@ -172,7 +172,7 @@ export default class CodeMirror extends EditorView {
} }
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
const plugin = createSearchHighlighter(this, searchTerm, matchCase, wholeWord); const plugin = createSearchHighlighter();
this.dispatch({ this.dispatch({
effects: this.searchHighlightCompartment.reconfigure(plugin) effects: this.searchHighlightCompartment.reconfigure(plugin)
}); });
@ -180,6 +180,7 @@ export default class CodeMirror extends EditorView {
// Wait for the plugin to activate in the next render cycle // Wait for the plugin to activate in the next render cycle
await new Promise(requestAnimationFrame); await new Promise(requestAnimationFrame);
const instance = this.plugin(plugin); const instance = this.plugin(plugin);
instance?.searchFor(searchTerm, matchCase, wholeWord);
if (instance) { if (instance) {
instance.scrollToMatchNearestSelection(); instance.scrollToMatchNearestSelection();
} }