fix(editor): update SQL search escaping and match counts
This commit is contained in:
parent
f89392de82
commit
77a5943729
|
|
@ -3,8 +3,9 @@ import { ref, nextTick, onBeforeUnmount, watch } from "vue";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import { SearchQuery, setSearchQuery, openSearchPanel as cmOpenSearchPanel, findNext as cmFindNext, findPrevious as cmFindPrevious, replaceNext as cmReplaceNext, replaceAll as cmReplaceAll } from "@codemirror/search";
|
||||
import { setSearchQuery, openSearchPanel as cmOpenSearchPanel, findNext as cmFindNext, findPrevious as cmFindPrevious, replaceNext as cmReplaceNext, replaceAll as cmReplaceAll } from "@codemirror/search";
|
||||
import { ChevronUp, ChevronDown, ChevronRight, X } from "@lucide/vue";
|
||||
import { createEditorSearchQuery } from "@/lib/editor/editorSearchQuery";
|
||||
|
||||
const props = defineProps<{
|
||||
view: EditorView | null;
|
||||
|
|
@ -26,17 +27,25 @@ const replaceInputRef = ref<HTMLInputElement>();
|
|||
const matchCountLimited = ref(false);
|
||||
|
||||
const SEARCH_UPDATE_DELAY_MS = 120;
|
||||
const DOCUMENT_SEARCH_UPDATE_DELAY_MS = 500;
|
||||
const MATCH_COUNT_LIMIT = 1000;
|
||||
|
||||
let searchUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let documentSearchUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function clearDocumentSearchUpdate() {
|
||||
if (!documentSearchUpdateTimer) return;
|
||||
clearTimeout(documentSearchUpdateTimer);
|
||||
documentSearchUpdateTimer = null;
|
||||
}
|
||||
|
||||
function dispatchSearchQuery() {
|
||||
const v = props.view;
|
||||
if (!v) return;
|
||||
const q = new SearchQuery({
|
||||
const q = createEditorSearchQuery({
|
||||
search: searchText.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
regexp: useRegex.value,
|
||||
useRegex: useRegex.value,
|
||||
replace: replaceText.value,
|
||||
});
|
||||
v.dispatch({ effects: setSearchQuery.of(q) });
|
||||
|
|
@ -48,7 +57,7 @@ function clearSearchQuery() {
|
|||
const selection = v.state.selection.main;
|
||||
v.dispatch({
|
||||
selection: EditorSelection.single(selection.head),
|
||||
effects: setSearchQuery.of(new SearchQuery({ search: "" })),
|
||||
effects: setSearchQuery.of(createEditorSearchQuery({ search: "", caseSensitive: false, useRegex: false })),
|
||||
});
|
||||
matchCount.value = 0;
|
||||
currentMatchIndex.value = 0;
|
||||
|
|
@ -64,10 +73,10 @@ function updateMatchInfo(autoSelect = false) {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const q = new SearchQuery({
|
||||
const q = createEditorSearchQuery({
|
||||
search: searchText.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
regexp: useRegex.value,
|
||||
useRegex: useRegex.value,
|
||||
});
|
||||
if (!q.valid) {
|
||||
matchCount.value = 0;
|
||||
|
|
@ -101,6 +110,7 @@ function updateMatchInfo(autoSelect = false) {
|
|||
}
|
||||
|
||||
function scheduleSearchUpdate(autoSelect = false) {
|
||||
clearDocumentSearchUpdate();
|
||||
if (searchUpdateTimer) {
|
||||
clearTimeout(searchUpdateTimer);
|
||||
searchUpdateTimer = null;
|
||||
|
|
@ -116,6 +126,15 @@ function scheduleSearchUpdate(autoSelect = false) {
|
|||
}, SEARCH_UPDATE_DELAY_MS);
|
||||
}
|
||||
|
||||
function scheduleDocumentSearchUpdate() {
|
||||
if (!searchVisible.value || !searchText.value) return;
|
||||
clearDocumentSearchUpdate();
|
||||
documentSearchUpdateTimer = setTimeout(() => {
|
||||
documentSearchUpdateTimer = null;
|
||||
updateMatchInfo();
|
||||
}, DOCUMENT_SEARCH_UPDATE_DELAY_MS);
|
||||
}
|
||||
|
||||
function openSearch(): boolean {
|
||||
searchVisible.value = true;
|
||||
const v = props.view;
|
||||
|
|
@ -146,6 +165,7 @@ function closeSearch() {
|
|||
const wasVisible = searchVisible.value;
|
||||
searchVisible.value = false;
|
||||
showReplace.value = false;
|
||||
clearDocumentSearchUpdate();
|
||||
const v = props.view;
|
||||
if (v) {
|
||||
clearSearchQuery();
|
||||
|
|
@ -204,13 +224,14 @@ watch(replaceText, () => {
|
|||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearDocumentSearchUpdate();
|
||||
if (searchUpdateTimer) {
|
||||
clearTimeout(searchUpdateTimer);
|
||||
searchUpdateTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ openSearch, openReplace, closeSearch });
|
||||
defineExpose({ openSearch, openReplace, closeSearch, scheduleDocumentSearchUpdate });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -2827,6 +2827,7 @@ onMounted(async () => {
|
|||
rectangularSelection({ eventFilter: (e: MouseEvent) => e.altKey || e.button === 1 }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
searchPanelRef.value?.scheduleDocumentSearchUpdate();
|
||||
if (isEditorComposing(update.view)) {
|
||||
pendingImeModelEmit = true;
|
||||
completionEpoch++;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { EditorState } from "@codemirror/state";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createEditorSearchQuery } from "@/lib/editor/editorSearchQuery";
|
||||
|
||||
function matchedText(search: string, useRegex: boolean): string[] {
|
||||
const state = EditorState.create({
|
||||
doc: String.raw`SELECT '\n' AS escaped;
|
||||
SELECT 1 AS actual_line_break;`,
|
||||
});
|
||||
const cursor = createEditorSearchQuery({ search, caseSensitive: false, useRegex }).getCursor(state);
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let result = cursor.next(); !result.done; result = cursor.next()) {
|
||||
matches.push(state.sliceDoc(result.value.from, result.value.to));
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
describe("editorSearchQuery", () => {
|
||||
it("treats escape sequences literally in normal search mode", () => {
|
||||
expect(matchedText(String.raw`\n`, false)).toEqual([String.raw`\n`]);
|
||||
});
|
||||
|
||||
it("allows regular expression mode to match actual line breaks", () => {
|
||||
expect(matchedText(String.raw`\n`, true)).toEqual(["\n"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { SearchQuery } from "@codemirror/search";
|
||||
|
||||
export interface EditorSearchQueryOptions {
|
||||
search: string;
|
||||
replace?: string;
|
||||
caseSensitive: boolean;
|
||||
useRegex: boolean;
|
||||
}
|
||||
|
||||
export function createEditorSearchQuery(options: EditorSearchQueryOptions): SearchQuery {
|
||||
return new SearchQuery({
|
||||
search: options.search,
|
||||
replace: options.replace ?? "",
|
||||
caseSensitive: options.caseSensitive,
|
||||
regexp: options.useRegex,
|
||||
literal: !options.useRegex,
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue