mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(structure): support search in DDL view
This commit is contained in:
@@ -14,6 +14,11 @@ const props = defineProps<{
|
||||
tone?: "app" | "editor";
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
open: [];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
@@ -271,6 +276,7 @@ function scheduleDocumentSearchUpdate() {
|
||||
|
||||
function openSearch(): boolean {
|
||||
searchVisible.value = true;
|
||||
emit("open");
|
||||
const v = props.view;
|
||||
if (v) {
|
||||
cmOpenSearchPanel(v);
|
||||
@@ -322,6 +328,7 @@ function closeSearch() {
|
||||
clearSearchQuery();
|
||||
v.focus();
|
||||
}
|
||||
if (wasVisible) emit("close");
|
||||
return wasVisible;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from "vue";
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, watch } from "vue";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
|
||||
@@ -21,6 +22,8 @@ import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useSettingsStore, type StructureEditorDensity } from "@/stores/settingsStore";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter";
|
||||
import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript";
|
||||
@@ -45,7 +48,7 @@ import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTabl
|
||||
import { getConcurrentIndexAvailability, concurrentIndexNamesInStatements, normalizeUnsupportedConcurrentIndexes, type ConcurrentIndexAvailability } from "@/lib/table/concurrentIndexAvailability";
|
||||
import { orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder";
|
||||
import { loadTableDataGridColumnOrder, notifyTableDataGridColumnOrderChanged, removeTableDataGridColumnOrder, saveTableDataGridColumnOrder, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { codeMirrorSqlDialectForConnection, connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { postgresListRolesSql, usersFromPostgresRolesResult } from "@/lib/database/databaseUserAdmin";
|
||||
import type { ColumnInfo, ConstraintInfo, TableInfo, TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
|
||||
import {
|
||||
@@ -94,9 +97,10 @@ import {
|
||||
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, normalizeCreateDatabaseCharsetKey, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import type { CreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isDark } = useTheme();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const store = useConnectionStore();
|
||||
const productionSafetyStore = useProductionSafetyStore();
|
||||
const queryStore = useQueryStore();
|
||||
@@ -162,23 +166,121 @@ const constraintsLoading = ref(false);
|
||||
const triggersLoading = ref(false);
|
||||
const ddlContent = ref("");
|
||||
const ddlLoading = ref(false);
|
||||
const ddlEditorContainer = ref<HTMLDivElement>();
|
||||
const ddlSearchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const ddlSearchOpen = ref(false);
|
||||
const ddlEditorView = shallowRef<EditorView | null>(null);
|
||||
let ddlEditorInitRequestId = 0;
|
||||
let ddlEditorScrollCleanup: (() => void) | null = null;
|
||||
const loadedMetadataFacets = new Set<ObjectMetadataFacet>();
|
||||
let structureEditorReady = false;
|
||||
const ddlPreRef = ref<HTMLPreElement | null>(null);
|
||||
function onDdlKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
||||
e.preventDefault();
|
||||
const el = ddlPreRef.value;
|
||||
if (!el) return;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
const ddlFetched = ref(false);
|
||||
|
||||
function ddlEditorDocument(): string {
|
||||
return ddlContent.value || t("structureEditor.emptyReadonly");
|
||||
}
|
||||
|
||||
function destroyDdlEditor() {
|
||||
ddlEditorInitRequestId += 1;
|
||||
ddlEditorScrollCleanup?.();
|
||||
ddlEditorScrollCleanup = null;
|
||||
ddlEditorView.value?.destroy();
|
||||
ddlEditorView.value = null;
|
||||
}
|
||||
|
||||
function updateDdlEditorContent(content: string): boolean {
|
||||
const view = ddlEditorView.value;
|
||||
if (!view) return false;
|
||||
if (view.state.doc.toString() !== content) {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: content },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function observeDdlEditorScroll(view: EditorView) {
|
||||
ddlEditorScrollCleanup?.();
|
||||
const scrollDOM = view.scrollDOM;
|
||||
const onScroll = (event: Event) => onStructureContentScroll("ddl", event);
|
||||
scrollDOM.addEventListener("scroll", onScroll, { passive: true });
|
||||
ddlEditorScrollCleanup = () => scrollDOM.removeEventListener("scroll", onScroll);
|
||||
}
|
||||
|
||||
async function initDdlEditor(content: string) {
|
||||
const container = ddlEditorContainer.value;
|
||||
if (!container) return;
|
||||
|
||||
const existingView = ddlEditorView.value;
|
||||
if (existingView?.dom.parentElement === container) {
|
||||
updateDdlEditorContent(content);
|
||||
existingView.focus();
|
||||
return;
|
||||
}
|
||||
if (existingView) destroyDdlEditor();
|
||||
|
||||
const requestId = ++ddlEditorInitRequestId;
|
||||
const [{ EditorView, keymap }, { EditorState, Prec }, langSql, { basicSetup }, { search: cmSearch }] = await Promise.all([import("@codemirror/view"), import("@codemirror/state"), import("@codemirror/lang-sql"), import("codemirror"), import("@codemirror/search")]);
|
||||
if (requestId !== ddlEditorInitRequestId || activeTab.value !== "ddl" || loading.value || ddlLoading.value || ddlEditorContainer.value !== container) return;
|
||||
|
||||
const editorSettings = settingsStore.editorSettings;
|
||||
const themeExt = await loadEditorTheme(editorSettings.theme, isDark.value ? "dark" : "light", undefined, themePalette.value);
|
||||
if (requestId !== ddlEditorInitRequestId || activeTab.value !== "ddl" || loading.value || ddlLoading.value || ddlEditorContainer.value !== container) return;
|
||||
|
||||
const fontExt = editorFontTheme(EditorView, editorSettings.fontSize, editorSettings.fontFamily, { fixedHeight: true, scrollable: true });
|
||||
const dialect = createDbxCodeMirrorSqlDialect(langSql, codeMirrorSqlDialectForConnection(connection.value), databaseType.value, connection.value?.driver_profile);
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions: [
|
||||
cmSearch({
|
||||
top: true,
|
||||
createPanel: () => {
|
||||
const dom = document.createElement("span");
|
||||
dom.style.display = "none";
|
||||
return { dom };
|
||||
},
|
||||
scrollToMatch: (range) => EditorView.scrollIntoView(range, { y: "center" }),
|
||||
}),
|
||||
basicSetup,
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
langSql.sql({ dialect }),
|
||||
themeExt,
|
||||
fontExt,
|
||||
Prec.highest(keymap.of([{ key: "Mod-f", run: () => ddlSearchPanelRef.value?.openSearch() ?? false, preventDefault: true }])),
|
||||
EditorView.theme({
|
||||
"&.cm-focused": { outline: "none" },
|
||||
".cm-content": {
|
||||
cursor: "text",
|
||||
padding: "0.75rem",
|
||||
userSelect: "text",
|
||||
WebkitUserSelect: "text",
|
||||
},
|
||||
".cm-line": {
|
||||
userSelect: "text",
|
||||
WebkitUserSelect: "text",
|
||||
},
|
||||
}),
|
||||
EditorState.readOnly.of(true),
|
||||
],
|
||||
});
|
||||
const editorView = new EditorView({ state, parent: container });
|
||||
if (requestId !== ddlEditorInitRequestId || activeTab.value !== "ddl" || loading.value || ddlLoading.value || ddlEditorContainer.value !== container) {
|
||||
editorView.destroy();
|
||||
return;
|
||||
}
|
||||
ddlEditorView.value = editorView;
|
||||
observeDdlEditorScroll(editorView);
|
||||
editorView.focus();
|
||||
restoreStructureScrollPosition("ddl");
|
||||
}
|
||||
|
||||
function scheduleDdlEditorInit() {
|
||||
void nextTick(() => {
|
||||
if (activeTab.value !== "ddl" || loading.value || ddlLoading.value) return;
|
||||
void initDdlEditor(ddlEditorDocument());
|
||||
});
|
||||
}
|
||||
|
||||
function ddlRequest() {
|
||||
return {
|
||||
connectionId: props.connectionId,
|
||||
@@ -191,6 +293,7 @@ function ddlRequest() {
|
||||
|
||||
async function fetchDdl(force = false) {
|
||||
if (!props.connectionId || !props.database || !props.tableName || (!force && ddlFetched.value) || !tableMetadataCapabilities.value.ddl) return;
|
||||
if (force) destroyDdlEditor();
|
||||
ddlLoading.value = true;
|
||||
try {
|
||||
const { ddl } = await loadObjectDdl(ddlRequest(), { force });
|
||||
@@ -1141,6 +1244,11 @@ function restoreStructureScrollPosition(tab = activeTab.value) {
|
||||
const position = structureScrollPositions.value[tab];
|
||||
if (!position) return;
|
||||
nextTick(() => {
|
||||
if (tab === "ddl" && ddlEditorView.value) {
|
||||
ddlEditorView.value.scrollDOM.scrollTop = Math.max(0, position.scrollTop);
|
||||
ddlEditorView.value.scrollDOM.scrollLeft = Math.max(0, position.scrollLeft);
|
||||
return;
|
||||
}
|
||||
const scroller = structureScrollerForTab(tab);
|
||||
if (!scroller) return;
|
||||
scroller.scrollTop = Math.max(0, position.scrollTop);
|
||||
@@ -3441,6 +3549,7 @@ function isPlainModDeleteShortcut(event: KeyboardEvent): boolean {
|
||||
}
|
||||
|
||||
function onStructureEditorKeydown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented) return;
|
||||
const focusedColumn = activeTab.value === "columns" ? focusedEditableColumn(event.target) : undefined;
|
||||
if (focusedColumn && isShiftEnterShortcut(event) && canAddColumn.value) {
|
||||
event.preventDefault();
|
||||
@@ -3465,6 +3574,7 @@ function onStructureEditorKeydown(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (activeTab.value === "columns") focusColumnSearch();
|
||||
else if (activeTab.value === "ddl") ddlSearchPanelRef.value?.openSearch();
|
||||
return;
|
||||
}
|
||||
if (isPlainModShortcut(event, "s")) {
|
||||
@@ -3547,6 +3657,7 @@ onActivated(() => {
|
||||
});
|
||||
}
|
||||
restoreStructureScrollPosition();
|
||||
if (activeTab.value === "ddl") scheduleDdlEditorInit();
|
||||
});
|
||||
onDeactivated(() => {
|
||||
unregisterStructureEditorShortcuts();
|
||||
@@ -3554,6 +3665,7 @@ onDeactivated(() => {
|
||||
structureHorizontalScrollbarResizeObserver?.disconnect();
|
||||
structureHorizontalScrollbarResizeObserver = null;
|
||||
stopStructureHorizontalScrollbarDrag();
|
||||
destroyDdlEditor();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clearCopySourceTableSearchTimer();
|
||||
@@ -3562,6 +3674,7 @@ onBeforeUnmount(() => {
|
||||
structureHorizontalScrollbarObserverGeneration += 1;
|
||||
structureHorizontalScrollbarResizeObserver?.disconnect();
|
||||
unregisterStructureEditorShortcuts();
|
||||
destroyDdlEditor();
|
||||
clearSqlPreviewState();
|
||||
if (columnHighlightTimer) window.clearTimeout(columnHighlightTimer);
|
||||
if (indexHighlightTimer) window.clearTimeout(indexHighlightTimer);
|
||||
@@ -3684,6 +3797,7 @@ watch(
|
||||
|
||||
watch(activeTab, () => {
|
||||
stopStructureHorizontalScrollbarDrag();
|
||||
if (activeTab.value !== "ddl") destroyDdlEditor();
|
||||
clearColumnSelection();
|
||||
highlightedColumnId.value = null;
|
||||
highlightedIndexId.value = null;
|
||||
@@ -3754,12 +3868,8 @@ async function loadActiveTableStructureMetadataIfNeeded() {
|
||||
|
||||
watch([activeTab, loading, secondaryMetadataLoading], () => void loadActiveTableStructureMetadataIfNeeded(), { flush: "sync" });
|
||||
|
||||
watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
if (tab === "ddl" && !loading) {
|
||||
void nextTick(() => {
|
||||
ddlPreRef.value?.focus();
|
||||
});
|
||||
}
|
||||
watch([activeTab, loading, ddlLoading, ddlContent], ([tab, structureIsLoading, ddlIsLoading]) => {
|
||||
if (tab === "ddl" && !structureIsLoading && !ddlIsLoading) scheduleDdlEditorInit();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4651,11 +4761,12 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<Button v-if="ddlContent" variant="outline" size="sm" class="absolute right-3 top-3 z-10 h-7 gap-1 px-2" :title="t('grid.copyDdl')" @click="copyDdlContent">
|
||||
<Button v-if="ddlContent && !ddlSearchOpen" variant="outline" size="sm" class="absolute right-3 top-3 z-10 h-7 gap-1 px-2" :title="t('grid.copyDdl')" @click="copyDdlContent">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
{{ t("grid.copyDdl") }}
|
||||
</Button>
|
||||
<pre ref="ddlPreRef" tabindex="0" class="m-0 min-h-0 flex-1 whitespace-pre p-3 font-mono text-xs leading-5 select-text outline-none" v-html="ddlContent ? (sqlHighlighter?.(ddlContent) ?? ddlContent) : t('structureEditor.emptyReadonly')" @keydown="onDdlKeydown"></pre>
|
||||
<div ref="ddlEditorContainer" class="structure-ddl-editor h-full min-h-full min-w-0 w-full"></div>
|
||||
<EditorSearchPanel v-if="ddlEditorView" ref="ddlSearchPanelRef" :view="ddlEditorView" @open="ddlSearchOpen = true" @close="ddlSearchOpen = false" />
|
||||
</template>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -4826,6 +4937,27 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.structure-ddl-editor :deep(.cm-editor) {
|
||||
min-height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.structure-ddl-editor :deep(.cm-content),
|
||||
.structure-ddl-editor :deep(.cm-line) {
|
||||
cursor: text;
|
||||
user-select: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
}
|
||||
|
||||
.structure-ddl-editor :deep(.cm-selectionBackground),
|
||||
.structure-ddl-editor :deep(.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground) {
|
||||
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35)) !important;
|
||||
}
|
||||
|
||||
.structure-ddl-editor :deep(.cm-content ::selection) {
|
||||
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35)) !important;
|
||||
}
|
||||
|
||||
.structure-table-scroller::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 0;
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, nextTick, type App } from "vue";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { search as cmSearch } from "@codemirror/search";
|
||||
import { EditorState, Prec } from "@codemirror/state";
|
||||
import { EditorView, keymap } from "@codemirror/view";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { collectEditorSearchMatches, createEditorSearchQuery } from "@/lib/editor/editorSearchQuery";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
|
||||
vi.mock("vue-i18n", () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: {
|
||||
regexMaxMatchCount: 1000,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const ddl = `CREATE TABLE users (
|
||||
id BIGINT,
|
||||
username VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
created_at DATETIME
|
||||
);`;
|
||||
type SearchPanelHandle = {
|
||||
openSearch: () => boolean;
|
||||
closeSearch: () => boolean;
|
||||
};
|
||||
|
||||
let editorHost: HTMLDivElement;
|
||||
let panelHost: HTMLDivElement;
|
||||
let editorView: EditorView;
|
||||
let panelApp: App;
|
||||
let panel: SearchPanelHandle;
|
||||
|
||||
beforeEach(async () => {
|
||||
editorHost = document.createElement("div");
|
||||
panelHost = document.createElement("div");
|
||||
document.body.append(editorHost, panelHost);
|
||||
|
||||
let panelHandle: SearchPanelHandle | null = null;
|
||||
const state = EditorState.create({
|
||||
doc: ddl,
|
||||
extensions: [
|
||||
cmSearch({
|
||||
top: true,
|
||||
createPanel: () => {
|
||||
const dom = document.createElement("span");
|
||||
dom.style.display = "none";
|
||||
return { dom };
|
||||
},
|
||||
}),
|
||||
basicSetup,
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorState.readOnly.of(true),
|
||||
Prec.highest(keymap.of([{ key: "Mod-f", run: () => panelHandle?.openSearch() ?? false, preventDefault: true }])),
|
||||
],
|
||||
});
|
||||
editorView = new EditorView({ state, parent: editorHost });
|
||||
|
||||
panelApp = createApp(EditorSearchPanel, { view: editorView });
|
||||
panel = panelApp.mount(panelHost) as unknown as SearchPanelHandle;
|
||||
panelHandle = panel;
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
panelApp.unmount();
|
||||
editorView.destroy();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function searchInput(): HTMLInputElement {
|
||||
const input = panelHost.querySelector<HTMLInputElement>(".editor-search-panel input");
|
||||
if (!input) throw new Error("Missing editor search input");
|
||||
return input;
|
||||
}
|
||||
|
||||
function searchResultLabel(): HTMLElement {
|
||||
const label = panelHost.querySelector<HTMLElement>('[aria-live="polite"]');
|
||||
if (!label) throw new Error("Missing editor search result label");
|
||||
return label;
|
||||
}
|
||||
|
||||
async function openSearchAndFind(text: string) {
|
||||
expect(panel.openSearch()).toBe(true);
|
||||
await nextTick();
|
||||
const input = searchInput();
|
||||
input.value = text;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await vi.waitFor(() => expect(searchResultLabel().textContent).toContain("/2"));
|
||||
return input;
|
||||
}
|
||||
|
||||
describe("TableStructureEditor DDL search", () => {
|
||||
it("routes Mod-f to the shared search panel and prevents browser find", async () => {
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey: true,
|
||||
key: "f",
|
||||
});
|
||||
|
||||
editorView.contentDOM.dispatchEvent(event);
|
||||
await nextTick();
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(panelHost.querySelector(".editor-search-panel")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("finds both VARCHAR matches without changing the read-only DDL", async () => {
|
||||
const before = editorView.state.doc.toString();
|
||||
const input = await openSearchAndFind("VARCHAR");
|
||||
const query = createEditorSearchQuery({ search: "VARCHAR", caseSensitive: false, useRegex: false });
|
||||
|
||||
expect(collectEditorSearchMatches(query, editorView.state, 0, editorView.state.doc.length)).toHaveLength(2);
|
||||
expect(searchResultLabel().textContent).toContain("/2");
|
||||
expect(editorView.state.doc.toString()).toBe(before);
|
||||
expect(editorView.state.facet(EditorState.readOnly)).toBe(true);
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter" }));
|
||||
await nextTick();
|
||||
expect(editorView.state.selection.main.from).toBe(ddl.lastIndexOf("VARCHAR"));
|
||||
});
|
||||
|
||||
it("supports Escape and Ctrl+A in the DDL editor", async () => {
|
||||
const input = await openSearchAndFind("VARCHAR");
|
||||
const escape = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Escape" });
|
||||
input.dispatchEvent(escape);
|
||||
await nextTick();
|
||||
expect(escape.defaultPrevented).toBe(true);
|
||||
await vi.waitFor(() => expect(panelHost.querySelector(".editor-search-panel")).toBeNull());
|
||||
|
||||
const selectAll = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, ctrlKey: true, key: "a" });
|
||||
editorView.contentDOM.dispatchEvent(selectAll);
|
||||
expect(editorView.state.selection.main.from).toBe(0);
|
||||
expect(editorView.state.selection.main.to).toBe(editorView.state.doc.length);
|
||||
});
|
||||
});
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../TableStructureEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("TableStructureEditor DDL search wiring", () => {
|
||||
it("uses a read-only CodeMirror viewer and the shared search panel", () => {
|
||||
expect(source).toContain('import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";');
|
||||
expect(source).toContain('key: "Mod-f"');
|
||||
expect(source).toContain('else if (activeTab.value === "ddl") ddlSearchPanelRef.value?.openSearch();');
|
||||
expect(source).toContain("EditorState.readOnly.of(true)");
|
||||
expect(source).not.toContain("ddlPreRef");
|
||||
expect(source).not.toContain("onDdlKeydown");
|
||||
});
|
||||
|
||||
it("refreshes and disposes the DDL editor across its component lifecycle", () => {
|
||||
expect(source).toContain("if (force) destroyDdlEditor();");
|
||||
expect(source).toContain("observeDdlEditorScroll(editorView);");
|
||||
expect(source).toContain("ddlEditorView.value.scrollDOM.scrollTop");
|
||||
expect(source).toContain('if (activeTab.value !== "ddl") destroyDdlEditor();');
|
||||
expect(source).toContain("onDeactivated(() => {");
|
||||
expect(source).toContain("onBeforeUnmount(() => {");
|
||||
expect(source.match(/destroyDdlEditor\(\);/g)?.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user