perf(table-info): mark DDL search hits on the rendered DOM

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 14:18:54 +08:00
1 parent 98101d20af
commit 70c6ce4c28
6 files changed
+172 -15

No files matched your search

+19 -14
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import { applyDdlSearchMarks } from "@/lib/sql/ddlSearchMarks";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { useUpdateBlocker } from "@/lib/app/updatePreparation";
@@ -10492,6 +10493,7 @@ watch([activeTableInfoTab, ddlLoading], ([tab, loading]) => {
}
void nextTick(() => {
ddlPreRef.value?.focus();
applyDdlSearchMarks(ddlPreRef.value, searchQuery.value);
syncDdlSearchMatches(true);
});
});
@@ -11471,24 +11473,27 @@ const filteredConstraints = computed(() => {
return base.filter((c) => c.name.toLowerCase().includes(q) || c.constraint_type.toLowerCase().includes(q) || c.columns.some((col) => col.toLowerCase().includes(q)) || c.definition.toLowerCase().includes(q));
});
const filteredDdlContent = computed(() => {
if (!ddlContent.value) return "";
const html = highlight(ddlContent.value);
if (!searchQuery.value) return html;
const escaped = searchQuery.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`(${escaped})`, "gi");
// Match only text between > and < (text nodes), then replace the search term within those spans
return html.replace(/>([^<]*)</g, (_, text) => {
return `>${text.replace(regex, '<mark class="ddl-search-match">$1</mark>')}<`;
});
});
// The highlighted DDL only changes when the statement itself changes: the
// search marks are applied to the rendered DOM instead, so typing in the DDL
// search box no longer re-highlights (or re-parses) the whole document (#9212).
const highlightedDdlContent = computed(() => (ddlContent.value ? highlight(ddlContent.value) : ""));
watch(
[filteredDdlContent, searchQuery],
highlightedDdlContent,
async () => {
await nextTick();
applyDdlSearchMarks(ddlPreRef.value, searchQuery.value);
syncDdlSearchMatches(true);
},
{ flush: "post" },
);
watch(
searchQuery,
async () => {
ddlSearchMatchIndex.value = 0;
await nextTick();
applyDdlSearchMarks(ddlPreRef.value, searchQuery.value);
syncDdlSearchMatches(true);
},
{ flush: "post" },
@@ -13616,7 +13621,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
tabindex="0"
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text outline-none"
:class="settingsStore.editorSettings.tableDdlWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
v-html="filteredDdlContent"
v-html="highlightedDdlContent"
@keydown="onDdlKeydown"
></pre>
</template>
@@ -6,7 +6,18 @@ const metadataLoaderSource = readFileSync(new URL("../../../composables/useDataG
describe("DataGrid DDL search navigation", () => {
it("resets navigation when the raw search query changes", () => {
expect(dataGridSource).toMatch(/watch\(\s*\[filteredDdlContent, searchQuery\],/);
expect(dataGridSource).toMatch(/watch\(\s*searchQuery,\s*async \(\) => \{\s*ddlSearchMatchIndex\.value = 0;/);
});
it("highlights the DDL once and marks the rendered DOM for the search", () => {
// #9212: re-highlighting (and re-parsing) the whole DDL on every keystroke
// froze the window, so the highlighted HTML only depends on the DDL itself
// and the search marks are applied to the already-rendered <pre>.
expect(dataGridSource).toMatch(/const highlightedDdlContent = computed\(\(\) => \(ddlContent\.value \? highlight\(ddlContent\.value\) : ""\)\);/);
expect(dataGridSource).toContain('v-html="highlightedDdlContent"');
expect(dataGridSource).toContain('import { applyDdlSearchMarks } from "@/lib/sql/ddlSearchMarks"');
expect(dataGridSource).not.toContain("filteredDdlContent");
expect(dataGridSource.match(/applyDdlSearchMarks\(ddlPreRef\.value, searchQuery\.value\)/g) ?? []).toHaveLength(3);
});
it("uses the shared DDL cache policy and exposes refresh controls", () => {