From e62ad97bcdf7d45fc4460414801483e9dafafb59 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 23 May 2026 11:48:56 +0800 Subject: [PATCH] feat(editor): enhance SQL completion icons and styles --- .../src/components/diff/DataCompareDialog.vue | 2 +- .../src/components/editor/QueryEditor.vue | 16 +++- apps/desktop/src/lib/editorThemes.ts | 94 ++++++++++++++++++- apps/desktop/src/styles/globals.css | 3 + .../app-tests/editorCompletionTheme.test.ts | 24 ++++- 5 files changed, 125 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index 597db9ae1..757c31cd7 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -138,7 +138,7 @@ const keyColumns = computed(() => .filter(Boolean), ); const rowLimitNumber = computed(() => Number(rowLimit.value) || 1000); -const comparedResults = computed(() => batchResults.value.filter((item) => item.status !== "error")); + const sameTableCount = computed(() => batchResults.value.filter((item) => item.status === "same").length); const differentTableCount = computed(() => batchResults.value.filter((item) => item.status === "different").length); const failedTableCount = computed(() => batchResults.value.filter((item) => item.status === "error").length); diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 2b8c97d9a..b8ffcf5f4 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -598,7 +598,11 @@ async function provideSqlCompletions( // If qualifier didn't match any table names, try it as a schema name let qualifierIsSchema = false; - if (completionContext.qualifier && completionContext.suggestTables && tables.length === 0) { + if ( + completionContext.qualifier && + tables.length === 0 && + (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions) + ) { const schemaTables = await connectionStore.listCompletionTables( props.connectionId, props.database, @@ -676,7 +680,13 @@ async function provideSqlCompletions( } const effectiveContext = qualifierIsSchema - ? { ...completionContext, qualifier: undefined, suggestTables: true, suggestColumns: false } + ? { + ...completionContext, + qualifier: undefined, + suggestTables: true, + suggestColumns: false, + exclusiveColumnSuggestions: false, + } : completionContext; const items = buildSqlCompletionItemsFromContext(effectiveContext, { @@ -692,7 +702,7 @@ async function provideSqlCompletions( item.type === "snippet" && item.apply ? codeMirrorSnippetCompletion(item.apply, { label: item.label, - type: "snippet", + type: item.type, detail: item.detail, boost: item.boost, }) diff --git a/apps/desktop/src/lib/editorThemes.ts b/apps/desktop/src/lib/editorThemes.ts index be8993750..ecb622d7e 100644 --- a/apps/desktop/src/lib/editorThemes.ts +++ b/apps/desktop/src/lib/editorThemes.ts @@ -3,10 +3,53 @@ import type { EditorTheme } from "@/stores/settingsStore"; import type { AppThemeAppearance } from "@/lib/appTheme"; type CodeMirrorStyleSpec = Parameters[0]; +type LucideIconNode = Array<[string, Record]>; export const EDITOR_FONT_SIZE_CSS_VAR = "--dbx-editor-font-size"; export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family"; +const TABLE_ICON: LucideIconNode = [ + ["path", { d: "M12 3v18" }], + ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], + ["path", { d: "M3 9h18" }], + ["path", { d: "M3 15h18" }], +]; + +const COLUMNS_ICON: LucideIconNode = [ + ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], + ["path", { d: "M12 3v18" }], +]; + +const KEYWORD_ICON: LucideIconNode = [ + ["path", { d: "m16 18 6-6-6-6" }], + ["path", { d: "m8 6-6 6 6 6" }], +]; + +const SNIPPET_ICON: LucideIconNode = [ + ["path", { d: "m15 10 5 5-5 5" }], + ["path", { d: "M4 4v7a4 4 0 0 0 4 4h12" }], +]; + +function encodeSvgIcon(iconNode: LucideIconNode): string { + const body = iconNode + .map( + ([tag, attrs]) => + `<${tag} ${Object.entries(attrs) + .map(([key, value]) => `${key}="${value}"`) + .join(" ")} />`, + ) + .join(""); + const svg = `${body}`; + return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; +} + +function lucideCompletionIconMask(iconNode: LucideIconNode) { + const mask = encodeSvgIcon(iconNode); + return { + "--dbx-completion-icon-mask": mask, + }; +} + /** Load a CodeMirror theme extension by theme name. */ export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance): Exclude { if (theme === "app") return appAppearance === "dark" ? "one-dark" : "vscode-light"; @@ -128,11 +171,52 @@ export function buildSqlCompletionThemeRules(): CodeMirrorStyleSpec { outline: "1px solid color-mix(in oklch, var(--primary) 22%, transparent)", }, ".cm-completionIcon": { - display: "none !important", - height: "0", - margin: "0", - paddingRight: "0 !important", - width: "0", + alignItems: "center", + display: "inline-flex", + flex: "0 0 15px", + height: "15px", + justifyContent: "center", + marginRight: "0.65em", + opacity: "1", + position: "relative", + overflow: "hidden", + width: "15px", + }, + ".cm-completionIcon:before": { + backgroundColor: "currentColor", + content: "''", + display: "block", + height: "14px", + position: "absolute", + WebkitMaskImage: "var(--dbx-completion-icon-mask)", + WebkitMaskPosition: "center", + WebkitMaskRepeat: "no-repeat", + WebkitMaskSize: "14px 14px", + maskImage: "var(--dbx-completion-icon-mask)", + maskPosition: "center", + maskRepeat: "no-repeat", + maskSize: "14px 14px", + width: "14px", + }, + ".cm-completionIcon:after": { + content: "'none'", + display: "none", + }, + ".cm-completionIcon-table": { + color: "color-mix(in oklch, var(--primary) 92%, var(--popover-foreground))", + ...lucideCompletionIconMask(TABLE_ICON), + }, + ".cm-completionIcon-column": { + color: "color-mix(in oklch, var(--blue-500, #3b82f6) 92%, var(--popover-foreground))", + ...lucideCompletionIconMask(COLUMNS_ICON), + }, + ".cm-completionIcon-keyword": { + color: "color-mix(in oklch, var(--orange-500, #f97316) 92%, var(--popover-foreground))", + ...lucideCompletionIconMask(KEYWORD_ICON), + }, + ".cm-completionIcon-snippet": { + color: "color-mix(in oklch, var(--emerald-500, #10b981) 92%, var(--popover-foreground))", + ...lucideCompletionIconMask(SNIPPET_ICON), }, ".cm-completionLabel": { color: "inherit", diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index 9c1a4348a..a72d389f9 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -153,6 +153,9 @@ html.disable-transitions *::after { } /* Splitpanes */ +.splitpanes--horizontal .splitpanes__pane { + transition: none !important; +} .splitpanes--vertical > .splitpanes__splitter { width: 3px !important; border-left: 1px solid var(--border); diff --git a/packages/app-tests/editorCompletionTheme.test.ts b/packages/app-tests/editorCompletionTheme.test.ts index 16ff7681b..6fb78008e 100644 --- a/packages/app-tests/editorCompletionTheme.test.ts +++ b/packages/app-tests/editorCompletionTheme.test.ts @@ -24,12 +24,26 @@ test("sql completion theme styles the autocomplete popup", () => { padding: "4px 0", }); assert.deepEqual(rules[".cm-completionIcon"], { - display: "none !important", - height: "0", - margin: "0", - paddingRight: "0 !important", - width: "0", + alignItems: "center", + display: "inline-flex", + flex: "0 0 15px", + height: "15px", + justifyContent: "center", + marginRight: "0.65em", + opacity: "1", + overflow: "hidden", + position: "relative", + width: "15px", }); + assert.equal(rules[".cm-completionIcon:before"]?.backgroundColor, "currentColor"); + assert.equal(rules[".cm-completionIcon:before"]?.content, "''"); + assert.equal(rules[".cm-completionIcon:before"]?.WebkitMaskSize, "14px 14px"); + assert.equal(rules[".cm-completionIcon:after"]?.display, "none"); + assert.equal(rules[".cm-completionIcon-table"]?.color, "color-mix(in oklch, var(--primary) 92%, var(--popover-foreground))"); + assert.equal(rules[".cm-completionIcon-column"]?.color, "color-mix(in oklch, var(--blue-500, #3b82f6) 92%, var(--popover-foreground))"); + assert.equal(rules[".cm-completionIcon-keyword"]?.color, "color-mix(in oklch, var(--orange-500, #f97316) 92%, var(--popover-foreground))"); + assert.equal(rules[".cm-completionIcon-keyword"]?.["--dbx-completion-icon-mask"]?.includes("m16%2018%206-6-6-6"), true); + assert.equal(rules[".cm-completionIcon-snippet"]?.color, "color-mix(in oklch, var(--emerald-500, #10b981) 92%, var(--popover-foreground))"); assert.deepEqual(rules[".cm-completionLabel"], { color: "inherit", fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, var(--font-mono, monospace))`,