feat(editor): enable search in embedded source views
This commit is contained in:
parent
3f228a04b0
commit
094d1fefde
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
24
apps/desktop/src/components/editor/__tests__/QueryEditorSearchKeymap.spec.ts
vendored
Normal file
24
apps/desktop/src/components/editor/__tests__/QueryEditorSearchKeymap.spec.ts
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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(),
|
||||
},
|
||||
];
|
||||
}
|
||||
Loading…
Reference in New Issue