feat(editor): follow app theme

This commit is contained in:
t8y2 2026-05-22 00:41:49 +08:00
parent 3dc4f40e7f
commit 355e09ccf4
9 changed files with 81 additions and 14 deletions

View File

@ -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<HTMLDivElement>();
const previewView = shallowRef<EditorViewType | null>(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,

View File

@ -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<HTMLDivElement>();
const view = shallowRef<EditorViewType | null>(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),

View File

@ -7,7 +7,7 @@ import {
Globe,
Moon,
Sun,
Monitor,
SunMoon,
Check,
History,
Bot,
@ -223,7 +223,7 @@ function onToolbarDblClick(e: MouseEvent) {
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('toolbar.theme')">
<Monitor v-if="themeMode === 'system'" class="h-4 w-4" />
<SunMoon v-if="themeMode === 'system'" class="h-4 w-4" />
<Moon v-else-if="isDark" class="h-4 w-4" />
<Sun v-else class="h-4 w-4" />
</Button>
@ -252,7 +252,7 @@ function onToolbarDblClick(e: MouseEvent) {
:class="{ 'bg-accent': themeMode === 'system' }"
@select="emit('set-theme-mode', 'system')"
>
<Monitor class="h-4 w-4" />
<SunMoon class="h-4 w-4" />
{{ t("toolbar.themeSystem") }}
<Check v-if="themeMode === 'system'" class="ml-auto h-4 w-4" />
</DropdownMenuItem>

View File

@ -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<typeof import("@codemirror/view").EditorView.theme>[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<Extension> {
switch (theme) {
export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAppearance): Exclude<EditorTheme, "app"> {
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<Extension> {
const resolvedTheme = resolveEditorTheme(theme, appAppearance);
switch (resolvedTheme) {
case "one-dark":
return (await import("@codemirror/theme-one-dark")).oneDark;
case "vscode-dark":

View File

@ -148,6 +148,7 @@ function inferAiProviderFromConfig(config: Partial<AiConfig> | 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<EditorTheme>(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<EditorSettings>): 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,

View File

@ -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, /<SunMoon v-if="themeMode === 'system'"/);
assert.match(source, /<SunMoon class="h-4 w-4" \/>/);
assert.doesNotMatch(source, /Monitor/);
});

View File

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

View File

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

View File

@ -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({});