feat(editor): enable search in embedded source views

This commit is contained in:
t8y2 2026-07-23 21:53:51 +08:00
parent 3f228a04b0
commit 094d1fefde
4 changed files with 96 additions and 1 deletions

View File

@ -60,6 +60,7 @@ import {
} from "@/lib/editor/queryEditorTableDrop";
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme, editorFontTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes";
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
@ -3680,6 +3681,11 @@ onMounted(async () => {
buildResultSourceRangeExtension(),
Prec.highest(
keymap.of([
...createQueryEditorSearchKeymap({
openSearch,
openReplace,
isReadOnly: () => !!props.readOnly,
}),
{ key: "'", run: handleSqlSingleQuote },
{ key: "Tab", run: handleTab },
{
@ -4305,6 +4311,7 @@ function openSearch(): boolean {
}
function openReplace(): boolean {
if (props.readOnly) return false;
return searchPanelRef.value?.openReplace() ?? false;
}

View File

@ -1,10 +1,51 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
const editorSearchPanelSource = readFileSync(new URL("../EditorSearchPanel.vue", import.meta.url), "utf8");
const queryEditorSource = readFileSync(new URL("../QueryEditor.vue", import.meta.url), "utf8");
const contentAreaSource = readFileSync(new URL("../../layout/ContentArea.vue", import.meta.url), "utf8");
describe("EditorSearchPanel corner style", () => {
it("uses the configurable five-pixel radius token for editor inputs", () => {
expect(editorSearchPanelSource).toContain("border-radius: var(--dbx-radius-fixed-5);");
});
});
describe("QueryEditor search shortcuts", () => {
it("opens search and replace in editable editors", () => {
const openSearch = vi.fn(() => true);
const openReplace = vi.fn(() => true);
const bindings = createQueryEditorSearchKeymap({ openSearch, openReplace, isReadOnly: () => false });
expect(bindings.map(({ key, preventDefault }) => ({ key, preventDefault }))).toEqual([
{ key: "Mod-f", preventDefault: true },
{ key: "Mod-h", preventDefault: true },
]);
expect(bindings[0]?.run?.({} as never)).toBe(true);
expect(bindings[1]?.run?.({} as never)).toBe(true);
expect(openSearch).toHaveBeenCalledOnce();
expect(openReplace).toHaveBeenCalledOnce();
});
it("allows search but consumes replace without opening it in read-only editors", () => {
const openSearch = vi.fn(() => true);
const openReplace = vi.fn(() => true);
const bindings = createQueryEditorSearchKeymap({ openSearch, openReplace, isReadOnly: () => true });
expect(bindings[0]?.run?.({} as never)).toBe(true);
expect(bindings[1]?.run?.({} as never)).toBe(true);
expect(openSearch).toHaveBeenCalledOnce();
expect(openReplace).not.toHaveBeenCalled();
});
it("keeps the existing search navigation and read-only replace guards", () => {
expect(queryEditorSource).toMatch(/Prec\.highest\(\s*keymap\.of\(\[\s*\.\.\.createQueryEditorSearchKeymap/);
expect(queryEditorSource).toMatch(/function openReplace\(\): boolean \{\s*if \(props\.readOnly\) return false;/);
expect(contentAreaSource).toContain('if (props.activeTab.mode === "query") return queryEditorRef.value?.openSearch() ?? false;');
expect(contentAreaSource).toContain("return queryEditorRef.value?.openReplace() ?? false;");
expect(queryEditorSource).toMatch(/key:\s*"Escape"/);
expect(editorSearchPanelSource).toContain('e.key === "Enter" && !e.shiftKey');
expect(editorSearchPanelSource).toContain('e.key === "Enter" && e.shiftKey');
});
});

View File

@ -0,0 +1,24 @@
// @vitest-environment happy-dom
import { EditorState, Prec } from "@codemirror/state";
import { EditorView, keymap, runScopeHandlers } from "@codemirror/view";
import { describe, expect, it, vi } from "vitest";
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
describe("QueryEditor search keymap precedence", () => {
it("runs the custom search binding before lower-priority CodeMirror bindings", () => {
const openSearch = vi.fn(() => true);
const lowerPrioritySearch = vi.fn(() => true);
const view = new EditorView({
parent: document.createElement("div"),
state: EditorState.create({
extensions: [keymap.of([{ key: "Mod-f", run: lowerPrioritySearch }]), Prec.highest(keymap.of(createQueryEditorSearchKeymap({ openSearch, openReplace: () => true, isReadOnly: () => false })))],
}),
});
expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "f", ctrlKey: true }), "editor")).toBe(true);
expect(openSearch).toHaveBeenCalledOnce();
expect(lowerPrioritySearch).not.toHaveBeenCalled();
view.destroy();
});
});

View File

@ -0,0 +1,23 @@
import type { KeyBinding } from "@codemirror/view";
interface QueryEditorSearchKeymapOptions {
openSearch: () => boolean;
openReplace: () => boolean;
isReadOnly: () => boolean;
}
export function createQueryEditorSearchKeymap(options: QueryEditorSearchKeymapOptions): KeyBinding[] {
return [
{
key: "Mod-f",
preventDefault: true,
run: options.openSearch,
},
{
key: "Mod-h",
preventDefault: true,
// Consume the shortcut in previews without exposing mutation controls.
run: () => options.isReadOnly() || options.openReplace(),
},
];
}