feat(editor): add query editing utilities

This commit is contained in:
JeaceLiu authored and GitHub committed 2026-07-17 16:32:50 +08:00
1 parent d689d78ab0
commit 782615586c
15 files changed
+989 -59

No files matched your search

@@ -1,3 +1,4 @@
import type { EditorState, TransactionSpec } from "@codemirror/state";
import { SearchQuery } from "@codemirror/search";
export interface EditorSearchQueryOptions {
@@ -16,3 +17,35 @@ export function createEditorSearchQuery(options: EditorSearchQueryOptions): Sear
literal: !options.useRegex,
});
}
export interface EditorSearchMatch {
from: number;
to: number;
}
export function collectEditorSearchMatches(query: SearchQuery, state: EditorState, from: number, to: number, limit = Number.POSITIVE_INFINITY): EditorSearchMatch[] {
const matches: EditorSearchMatch[] = [];
const cursor = query.getCursor(state);
for (let result = cursor.next(); !result.done; result = cursor.next()) {
if (result.value.from >= from && result.value.to <= to) {
matches.push({ from: result.value.from, to: result.value.to });
if (matches.length >= limit) break;
}
}
return matches;
}
export function replaceEditorSearchMatches(view: { dispatch(spec: TransactionSpec): void }, matches: readonly EditorSearchMatch[], replacement: (match: EditorSearchMatch) => string): boolean {
if (matches.length === 0) return false;
view.dispatch({
changes: matches.map((match) => ({
from: match.from,
to: match.to,
insert: replacement(match),
})),
});
return true;
}
@@ -0,0 +1,60 @@
import type { ChangeSpec, EditorState, SelectionRange, Text, TransactionSpec } from "@codemirror/state";
export interface DispatchableEditorView {
state: EditorState;
dispatch(spec: TransactionSpec): void;
}
export function replaceSelectedEditorText(view: DispatchableEditorView, insert: string): boolean {
if (view.state.readOnly) return false;
const selection = view.state.selection.main;
if (selection.empty) return false;
view.dispatch({
changes: { from: selection.from, to: selection.to, insert },
selection: { anchor: selection.from + insert.length },
scrollIntoView: true,
});
return true;
}
export function blankLineDeletionChanges(doc: Text, selection: SelectionRange): ChangeSpec[] {
if (doc.length === 0) return [];
const hasSelection = !selection.empty;
const firstLine = hasSelection ? doc.lineAt(selection.from).number : 1;
const lastSelectedPosition = hasSelection ? Math.max(selection.from, selection.to - 1) : doc.length;
const lastLine = hasSelection ? doc.lineAt(lastSelectedPosition).number : doc.lines;
const changes: ChangeSpec[] = [];
let lineNumber = firstLine;
while (lineNumber <= lastLine) {
const line = doc.line(lineNumber);
if (line.text.trim().length > 0) {
lineNumber += 1;
continue;
}
const firstBlankLine = line;
let lastBlankLine = line;
while (lineNumber < lastLine) {
const nextLine = doc.line(lineNumber + 1);
if (nextLine.text.trim().length > 0) break;
lineNumber += 1;
lastBlankLine = nextLine;
}
// Prefer the following separator so non-empty lines retain their existing newline.
if (lastBlankLine.to < doc.length) {
changes.push({ from: firstBlankLine.from, to: lastBlankLine.to + 1 });
} else if (firstBlankLine.from > 0) {
changes.push({ from: firstBlankLine.from - 1, to: lastBlankLine.to });
} else {
changes.push({ from: 0, to: doc.length });
}
lineNumber += 1;
}
return changes;
}