From 355e09ccf408f5f29f30bf8f78f7289432dc341e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 22 May 2026 00:41:49 +0800 Subject: [PATCH] feat(editor): follow app theme --- .../components/editor/EditorSettingsDialog.vue | 16 +++++++++++++--- .../src/components/editor/QueryEditor.vue | 14 ++++++++++---- .../desktop/src/components/layout/AppToolbar.vue | 6 +++--- apps/desktop/src/lib/editorThemes.ts | 15 +++++++++++++-- apps/desktop/src/stores/settingsStore.ts | 8 ++++++-- .../app-tests/appToolbarDropdownTooltip.test.ts | 9 +++++++++ packages/app-tests/editorThemeResolution.test.ts | 13 +++++++++++++ packages/app-tests/settingsDialogLayout.test.ts | 6 ++++++ packages/app-tests/settingsStore.test.ts | 8 ++++++++ 9 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 packages/app-tests/editorThemeResolution.test.ts diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 1a279d0a7..cfb37e349 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -20,9 +20,11 @@ import { DEFAULT_EDITOR_SETTINGS, type AiProvider, type AiApiStyle, + type EditorTheme, } from "@/stores/settingsStore"; import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes"; import { isTauriRuntime } from "@/lib/tauriRuntime"; +import { useTheme } from "@/composables/useTheme"; import { aiListModels, aiTestConnection, listSystemFonts, type AiModelInfo } from "@/lib/api"; import { eventToShortcut } from "@/lib/keyboardShortcuts"; import { @@ -34,9 +36,11 @@ import { } from "@/lib/shortcutRegistry"; import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebarTableNameDisplay"; import AiProviderLogo from "@/components/icons/AiProviderLogo.vue"; +import type { AppThemeAppearance } from "@/lib/appTheme"; const { t } = useI18n(); const settingsStore = useSettingsStore(); +const { isDark } = useTheme(); const props = defineProps<{ open: boolean; @@ -540,10 +544,16 @@ async function aiTestConn() { const previewRef = ref(); const previewView = shallowRef(null); -const previewSettings = computed(() => ({ +const previewSettings = computed<{ + fontFamily: string; + fontSize: number; + theme: EditorTheme; + appAppearance: AppThemeAppearance; +}>(() => ({ fontFamily: editFontFamily.value, fontSize: editFontSize.value, theme: editTheme.value, + appAppearance: isDark.value ? "dark" : "light", })); const previewSql = `SELECT u.id, u.name @@ -559,7 +569,7 @@ watch( async (ss) => { if (!previewView.value || !fontThemeComp || !themeComp || !editorViewModule) return; - const themeExt = await loadEditorTheme(ss.theme); + const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance); previewView.value.dispatch({ effects: [ themeComp.reconfigure(themeExt), @@ -600,7 +610,7 @@ watch(previewRef, async (el) => { themeComp = new Compartment(); const ss = previewSettings.value; - const themeExt = await loadEditorTheme(ss.theme); + const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance); const state = EditorState.create({ doc: previewSql, diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 758ab3241..52de46ac2 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -16,6 +16,7 @@ import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; +import { useTheme } from "@/composables/useTheme"; import { buildSqlCompletionItemsFromContext, getSqlFunctionSignatureHelp, @@ -68,6 +69,7 @@ const editorRef = ref(); const view = shallowRef(null); const connectionStore = useConnectionStore(); const settingsStore = useSettingsStore(); +const { isDark } = useTheme(); const MAX_COMPLETION_TABLES = 200; const liveFontSize = ref(settingsStore.editorSettings.fontSize); const gestureStartFontSize = ref(settingsStore.editorSettings.fontSize); @@ -98,6 +100,10 @@ let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null; let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion; +function editorThemeAppearance() { + return isDark.value ? "dark" : "light"; +} + // Completion cache let cachedTables: Array<{ name: string; schema?: string; type?: "table" | "view" }> = []; // Persistent column cache keyed by "schema.table" or "table" @@ -649,7 +655,7 @@ onMounted(async () => { keywords: (baseDialect.spec.keywords || "") + " " + extraKeywords, }); - const theme = await loadEditorTheme(ss.theme); + const theme = await loadEditorTheme(ss.theme, editorThemeAppearance()); const state = EditorState.create({ doc: props.modelValue, @@ -908,8 +914,8 @@ watch( // Reactively apply editor settings changes watch( - () => settingsStore.editorSettings, - async (ss) => { + [() => settingsStore.editorSettings, () => isDark.value], + async ([ss]) => { if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !runKeymapComp || !editorViewModule) { return; } @@ -917,7 +923,7 @@ watch( liveFontSize.value = ss.fontSize; } syncEditorFontCssVars(liveFontSize.value, ss.fontFamily); - const themeExt = await loadEditorTheme(ss.theme); + const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance()); view.value.dispatch({ effects: [ codeMirrorTheme.reconfigure(themeExt), diff --git a/apps/desktop/src/components/layout/AppToolbar.vue b/apps/desktop/src/components/layout/AppToolbar.vue index 4ccdf45a2..77b066c3f 100644 --- a/apps/desktop/src/components/layout/AppToolbar.vue +++ b/apps/desktop/src/components/layout/AppToolbar.vue @@ -7,7 +7,7 @@ import { Globe, Moon, Sun, - Monitor, + SunMoon, Check, History, Bot, @@ -223,7 +223,7 @@ function onToolbarDblClick(e: MouseEvent) { @@ -252,7 +252,7 @@ function onToolbarDblClick(e: MouseEvent) { :class="{ 'bg-accent': themeMode === 'system' }" @select="emit('set-theme-mode', 'system')" > - + {{ t("toolbar.themeSystem") }} diff --git a/apps/desktop/src/lib/editorThemes.ts b/apps/desktop/src/lib/editorThemes.ts index 7d1510742..a75aa457c 100644 --- a/apps/desktop/src/lib/editorThemes.ts +++ b/apps/desktop/src/lib/editorThemes.ts @@ -1,5 +1,6 @@ import type { Extension } from "@codemirror/state"; import type { EditorTheme } from "@/stores/settingsStore"; +import type { AppThemeAppearance } from "@/lib/appTheme"; type CodeMirrorStyleSpec = Parameters[0]; @@ -7,8 +8,18 @@ export const EDITOR_FONT_SIZE_CSS_VAR = "--dbx-editor-font-size"; export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family"; /** Load a CodeMirror theme extension by theme name. */ -export async function loadEditorTheme(theme: EditorTheme): Promise { - switch (theme) { +export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance): Exclude { + if (theme === "app") return appAppearance === "dark" ? "one-dark" : "vscode-light"; + return theme; +} + +/** Load a CodeMirror theme extension by theme name. */ +export async function loadEditorTheme( + theme: EditorTheme, + appAppearance: AppThemeAppearance = "dark", +): Promise { + const resolvedTheme = resolveEditorTheme(theme, appAppearance); + switch (resolvedTheme) { case "one-dark": return (await import("@codemirror/theme-one-dark")).oneDark; case "vscode-dark": diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 0a6d530fa..7f4697657 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -148,6 +148,7 @@ function inferAiProviderFromConfig(config: Partial | null | undefined) } export type EditorTheme = + | "app" | "one-dark" | "vscode-dark" | "vscode-light" @@ -178,6 +179,7 @@ export interface EditorSettings { } export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [ + { value: "app", label: "Follow app theme", dark: false }, { value: "one-dark", label: "One Dark", dark: true }, { value: "vscode-dark", label: "VS Dark+", dark: true }, { value: "vscode-light", label: "VS Light+", dark: false }, @@ -189,6 +191,8 @@ export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean } { value: "xcode", label: "Xcode", dark: false }, ]; +const EDITOR_THEME_VALUES = new Set(EDITOR_THEMES.map((theme) => theme.value)); + export const FONT_FAMILIES: { value: string; label: string }[] = [ { value: "'JetBrains Mono', 'Fira Code', monospace", label: "JetBrains Mono" }, { value: "'Fira Code', monospace", label: "Fira Code" }, @@ -202,7 +206,7 @@ export const FONT_FAMILIES: { value: string; label: string }[] = [ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: 13, - theme: "one-dark", + theme: "app", executeMode: "all", wordWrap: false, compactTabTitle: false, @@ -245,7 +249,7 @@ export function normalizeEditorSettings(settings: Partial): Edit return { fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily, fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize, - theme: settings.theme ?? DEFAULT_EDITOR_SETTINGS.theme, + theme: settings.theme && EDITOR_THEME_VALUES.has(settings.theme) ? settings.theme : DEFAULT_EDITOR_SETTINGS.theme, executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode, wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap, compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle, diff --git a/packages/app-tests/appToolbarDropdownTooltip.test.ts b/packages/app-tests/appToolbarDropdownTooltip.test.ts index a757bca9f..91801fbb3 100644 --- a/packages/app-tests/appToolbarDropdownTooltip.test.ts +++ b/packages/app-tests/appToolbarDropdownTooltip.test.ts @@ -20,3 +20,12 @@ test("toolbar theme and language menus use shadcn tooltip without nesting trigge assert.doesNotMatch(source, /group\/toolbar-tip/); assert.doesNotMatch(source, /group-hover\/toolbar-tip/); }); + +test("toolbar uses SunMoon for the system theme option", () => { + const source = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8"); + + assert.match(source, /SunMoon/); + assert.match(source, //); + assert.doesNotMatch(source, /Monitor/); +}); diff --git a/packages/app-tests/editorThemeResolution.test.ts b/packages/app-tests/editorThemeResolution.test.ts new file mode 100644 index 000000000..a8fd51158 --- /dev/null +++ b/packages/app-tests/editorThemeResolution.test.ts @@ -0,0 +1,13 @@ +import { strict as assert } from "node:assert"; +import test from "node:test"; +import { resolveEditorTheme } from "../../apps/desktop/src/lib/editorThemes.ts"; + +test("editor theme follows the resolved app appearance when configured to follow app theme", () => { + assert.equal(resolveEditorTheme("app", "light"), "vscode-light"); + assert.equal(resolveEditorTheme("app", "dark"), "one-dark"); +}); + +test("editor theme keeps explicit CodeMirror theme selections", () => { + assert.equal(resolveEditorTheme("nord", "light"), "nord"); + assert.equal(resolveEditorTheme("xcode", "dark"), "xcode"); +}); diff --git a/packages/app-tests/settingsDialogLayout.test.ts b/packages/app-tests/settingsDialogLayout.test.ts index 3f4609735..0fe0e89d5 100644 --- a/packages/app-tests/settingsDialogLayout.test.ts +++ b/packages/app-tests/settingsDialogLayout.test.ts @@ -38,6 +38,12 @@ test("settings dialog has a shortcuts category", () => { assert.match(shortcutSource, /settings\.shortcutToggleTranspose/); }); +test("settings editor theme preview can follow app appearance", () => { + assert.match(source, /useTheme/); + assert.match(source, /appAppearance: isDark\.value \? "dark" : "light"/); + assert.match(source, /loadEditorTheme\(ss\.theme, ss\.appAppearance\)/); +}); + test("shortcut settings capture custom keydown input instead of fixed select options", () => { assert.match(source, /onShortcutKeydown/); assert.match(source, /@keydown="\(event: KeyboardEvent\) => onShortcutKeydown/); diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index 86e5837bc..55f882507 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -23,6 +23,14 @@ test("normalizes saved query result page size", () => { assert.equal(normalizeEditorSettings({ pageSize: 0 }).pageSize, 100); }); +test("normalizes editor theme settings", () => { + assert.equal(DEFAULT_EDITOR_SETTINGS.theme, "app"); + assert.equal(normalizeEditorSettings({}).theme, "app"); + assert.equal(normalizeEditorSettings({ theme: "app" }).theme, "app"); + assert.equal(normalizeEditorSettings({ theme: "vscode-light" }).theme, "vscode-light"); + assert.equal(normalizeEditorSettings({ theme: "invalid" as any }).theme, DEFAULT_EDITOR_SETTINGS.theme); +}); + test("defaults shortcut settings", () => { const settings = normalizeEditorSettings({});