From c31d403af31398bb39e4e21ee6e11edd03853aab Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 23 Jul 2026 21:50:42 +0800 Subject: [PATCH] feat(grid): add DDL search navigation --- apps/desktop/src/components/grid/DataGrid.vue | 114 ++++++++++++++++-- .../grid/__tests__/DataGridDdlSearch.spec.ts | 10 ++ 2 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/components/grid/__tests__/DataGridDdlSearch.spec.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 3cf6ade2b..554cccaa4 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -8,6 +8,7 @@ import { Upload, Trash2, ChevronDown, + ChevronUp, ChevronLeft, ChevronRight, Search, @@ -6690,6 +6691,57 @@ const showTableInfo = ref(false); const activeTableInfoTab = ref(settingsStore.editorSettings.tableInfoActiveTab); const ddlContent = ref(""); const ddlPreRef = ref(null); +const ddlSearchMatchCount = ref(0); +const ddlSearchMatchIndex = ref(0); + +function scrollDdlSearchMatchIntoView(match: HTMLElement) { + const pre = ddlPreRef.value; + if (!pre) return; + + const preRect = pre.getBoundingClientRect(); + const matchRect = match.getBoundingClientRect(); + pre.scrollTop += matchRect.top - preRect.top - (pre.clientHeight - matchRect.height) / 2; + pre.scrollLeft += matchRect.left - preRect.left - (pre.clientWidth - matchRect.width) / 2; +} + +function syncDdlSearchMatches(scrollToActive = false) { + const pre = ddlPreRef.value; + if (!pre || activeTableInfoTab.value !== "ddl" || !searchQuery.value) { + ddlSearchMatchCount.value = 0; + ddlSearchMatchIndex.value = 0; + return; + } + + const matches = Array.from(pre.querySelectorAll("mark.ddl-search-match")); + ddlSearchMatchCount.value = matches.length; + if (matches.length === 0) { + ddlSearchMatchIndex.value = 0; + return; + } + + ddlSearchMatchIndex.value = Math.min(ddlSearchMatchIndex.value, matches.length - 1); + matches.forEach((match, index) => match.classList.toggle("ddl-search-match-active", index === ddlSearchMatchIndex.value)); + const activeMatch = matches[ddlSearchMatchIndex.value]; + if (scrollToActive && activeMatch) scrollDdlSearchMatchIntoView(activeMatch); +} + +function navigateDdlSearch(delta: -1 | 1) { + const count = ddlSearchMatchCount.value; + if (count === 0) return; + ddlSearchMatchIndex.value = (ddlSearchMatchIndex.value + delta + count) % count; + syncDdlSearchMatches(true); +} + +function onTableInfoSearchKeydown(e: KeyboardEvent) { + if (e.key === "Escape") { + searchQuery.value = ""; + return; + } + if (e.key !== "Enter" || activeTableInfoTab.value !== "ddl") return; + e.preventDefault(); + navigateDdlSearch(e.shiftKey ? -1 : 1); +} + function onDdlKeydown(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && e.key === "a") { e.preventDefault(); @@ -6745,11 +6797,16 @@ watch(activeTableInfoTab, () => { }); watch([activeTableInfoTab, ddlLoading], ([tab, loading]) => { - if (tab === "ddl" && !loading) { - void nextTick(() => { - ddlPreRef.value?.focus(); - }); + if (tab !== "ddl") return; + if (loading) { + ddlSearchMatchCount.value = 0; + ddlSearchMatchIndex.value = 0; + return; } + void nextTick(() => { + ddlPreRef.value?.focus(); + syncDdlSearchMatches(true); + }); }); watch( @@ -7216,10 +7273,20 @@ const filteredDdlContent = computed(() => { const regex = new RegExp(`(${escaped})`, "gi"); // Match only text between > and < (text nodes), then replace the search term within those spans return html.replace(/>([^<]*) { - return `>${text.replace(regex, "$1")}<`; + return `>${text.replace(regex, '$1')}<`; }); }); +watch( + [filteredDdlContent, searchQuery], + async () => { + ddlSearchMatchIndex.value = 0; + await nextTick(); + syncDdlSearchMatches(true); + }, + { flush: "post" }, +); + defineExpose({ useTransaction, transactionActive, @@ -8616,12 +8683,25 @@ const gridContextMenuItems = computed(() => {
-
- - - +
+
+ + + +
+
@@ -9725,4 +9805,16 @@ const gridContextMenuItems = computed(() => { color: rgb(213 111 44); color: oklch(0.65 0.15 50); } + +.ddl-code :deep(.ddl-search-match) { + border-radius: 2px; + background: var(--data-grid-cell-search-bg); + color: inherit; + padding: 0; +} + +.ddl-code :deep(.ddl-search-match-active) { + background: var(--data-grid-cell-current-search-bg); + outline: 1px solid var(--data-grid-cell-current-search-border); +} diff --git a/apps/desktop/src/components/grid/__tests__/DataGridDdlSearch.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridDdlSearch.spec.ts new file mode 100644 index 000000000..caba8dd6c --- /dev/null +++ b/apps/desktop/src/components/grid/__tests__/DataGridDdlSearch.spec.ts @@ -0,0 +1,10 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8"); + +describe("DataGrid DDL search navigation", () => { + it("resets navigation when the raw search query changes", () => { + expect(dataGridSource).toMatch(/watch\(\s*\[filteredDdlContent, searchQuery\],/); + }); +});