feat(editor): follow app theme
This commit is contained in:
parent
3dc4f40e7f
commit
355e09ccf4
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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/);
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue