feat(editor): enhance SQL completion icons and styles

This commit is contained in:
t8y2 2026-05-23 11:48:56 +08:00
parent d8650df323
commit e62ad97bcd
5 changed files with 125 additions and 14 deletions

View File

@ -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);

View File

@ -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,
})

View File

@ -3,10 +3,53 @@ import type { EditorTheme } from "@/stores/settingsStore";
import type { AppThemeAppearance } from "@/lib/appTheme";
type CodeMirrorStyleSpec = Parameters<typeof import("@codemirror/view").EditorView.theme>[0];
type LucideIconNode = Array<[string, Record<string, string>]>;
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
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<EditorTheme, "app"> {
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",

View File

@ -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);

View File

@ -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))`,