feat: custom theme system with PostgreSQL PL/pgSQL highlighting fix

- Fix PostgreSQL function highlighting by disabling doubleDollarQuotedStrings
  in extended PostgreSQL dialect, enabling PL/pgSQL syntax highlighting
  inside $ blocks
- Add complete custom editor theme system with multi-theme management
  (create, rename, duplicate, delete), visual color editor (12 colors),
  JSON import/export, real-time preview, and 12 preset color schemes
- Add background/foreground color customization with system theme defaults
- Optimize EditorSettingsDialog layout: 2-column grid with font selector
  taking available space and theme dropdown grouped with custom theme button
- Add i18n support for custom theme UI (en, zh-CN, zh-TW, es)
- Fix Tauri production build by adding custom-protocol feature
- Update .gitignore for temporary build artifacts

Closes t8y2#788

Co-authored-by: Sam <14344444@@qq.com>
This commit is contained in:
polempandSam authored and GitHub committed 2026-06-06 20:59:21 +08:00
1 parent 0b07f05f72
commit a067ee8d76
15 files changed
+1190 -43

No files matched your search

@@ -1,5 +1,15 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed } from "vue";
import {
ref,
onMounted,
onBeforeUnmount,
onActivated,
onDeactivated,
watch,
shallowRef,
computed,
nextTick,
} from "vue";
import { Play, Copy, TextSelect } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import type { CompletionContext } from "@codemirror/autocomplete";
@@ -1405,17 +1415,28 @@ onMounted(async () => {
],
});
const ss = settingsStore.editorSettings;
const baseDialect = props.dialect === "postgres" ? PostgreSQL : props.dialect === "sqlserver" ? MSSQL : MySQL;
const extraKeywords =
"PIVOT UNPIVOT EXCLUDE REPLACE QUALIFY ASOF POSITIONAL ANTI SEMI SAMPLE TABLESAMPLE STRUCT MAP LIST ARRAY LAMBDA UNNEST LATERAL FILTER RECURSIVE SUMMARIZE PRAGMA READ_CSV READ_PARQUET READ_JSON DESCRIBE SHOW COPY EXPORT IMPORT";
// PL/pgSQL extension: add procedural language keywords and built-in variables for PostgreSQL function/procedure bodies
const isPostgres = props.dialect === "postgres";
const plpgsqlKeywords = isPostgres ? "PERFORM" : "";
const plpgsqlTypes = isPostgres ? " RECORD JSON JSONB" : "";
const plpgsqlBuiltin = isPostgres
? "SQLERRM TG_NAME TG_WHEN TG_LEVEL TG_OP TG_RELID TG_RELNAME TG_TABLE_NAME TG_TABLE_SCHEMA TG_NARGS TG_ARGV"
: "";
const dialect = SQLDialect.define({
...baseDialect.spec,
keywords: (baseDialect.spec.keywords || "") + " " + extraKeywords,
keywords: [baseDialect.spec.keywords || "", extraKeywords, plpgsqlKeywords].filter(Boolean).join(" "),
types: [baseDialect.spec.types || "", plpgsqlTypes].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", plpgsqlBuiltin].filter(Boolean).join(" ") || undefined,
doubleDollarQuotedStrings: false,
});
const theme = await loadEditorTheme(ss.theme, editorThemeAppearance());
const initialSettings = settingsStore.editorSettings;
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors());
const activeLineHighlighter = ViewPlugin.fromClass(
class {
@@ -1496,7 +1517,7 @@ onMounted(async () => {
]),
),
runKeymapComp.of(runKeymapExtension(keymap)),
wordWrapComp.of(props.forceWordWrap || ss.wordWrap ? EditorView.lineWrapping : []),
wordWrapComp.of(props.forceWordWrap || initialSettings.wordWrap ? EditorView.lineWrapping : []),
readOnlyComp.of([EditorState.readOnly.of(!!props.readOnly), EditorView.editable.of(!props.readOnly)]),
rectangularSelection({ eventFilter: (e: MouseEvent) => e.altKey || e.button === 1 }),
EditorView.updateListener.of((update) => {
@@ -1518,7 +1539,7 @@ onMounted(async () => {
}
}),
fontThemeComp.of(
editorFontTheme(EditorView, liveFontSize.value, ss.fontFamily, {
editorFontTheme(EditorView, liveFontSize.value, initialSettings.fontFamily, {
fixedHeight: true,
scrollable: true,
}),
@@ -1678,12 +1699,23 @@ onMounted(async () => {
view.value = new EditorView({ state, parent: editorRef.value });
syncContextMenuState(view.value);
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
syncEditorFontCssVars(liveFontSize.value, initialSettings.fontFamily);
registerTableReferenceDropListener();
cachedTables = [];
cachedCompletionObjects = [];
scheduleSemanticDiagnostics();
// Ensure theme is applied with the latest settings after mount
void nextTick(async () => {
if (!view.value || !codeMirrorTheme) return;
const settings = settingsStore.editorSettings;
const themeColors = settings.theme === "custom" ? getCurrentCustomThemeColors() : settings.customThemeColors;
const themeExt = await loadEditorTheme(settings.theme, editorThemeAppearance(), themeColors);
view.value.dispatch({
effects: [codeMirrorTheme.reconfigure(themeExt)],
});
});
});
watch(
@@ -1748,6 +1780,16 @@ watch(
},
);
// Derive current custom theme colors from settingsStore
function getCurrentCustomThemeColors() {
const settings = settingsStore.editorSettings;
if (settings.theme !== "custom") return settings.customThemeColors;
const activeTheme =
settings.customThemes?.find((t: { id: string }) => t.id === settings.activeCustomThemeId) ||
settings.customThemes?.[0];
return activeTheme?.colors ?? settings.customThemeColors;
}
// Reactively apply editor settings changes
watch(
[() => settingsStore.editorSettings, () => isDark.value],
@@ -1759,7 +1801,8 @@ watch(
liveFontSize.value = ss.fontSize;
}
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance());
const themeColors = getCurrentCustomThemeColors();
const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors);
view.value.dispatch({
effects: [
codeMirrorTheme.reconfigure(themeExt),