This commit is contained in:
SuLea-IT 2026-06-11 10:47:42 +08:00 committed by GitHub
parent ddfbc2c3b5
commit 634e26e8e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1614 additions and 124 deletions

View File

@ -830,6 +830,13 @@ async function downloadWebDavSnapshot() {
});
}
const oldPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
const passwordMessage = ref("");
const passwordError = ref(false);
const changingPassword = ref(false);
watch(
() => props.open,
async (open) => {
@ -868,13 +875,6 @@ onMounted(() => {
void refreshWebDavPasswordStatus();
});
const oldPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
const passwordMessage = ref("");
const passwordError = ref(false);
const changingPassword = ref(false);
async function changePassword() {
if (newPassword.value !== confirmNewPassword.value) {
passwordMessage.value = t("auth.passwordMismatch");

View File

@ -1,27 +1,40 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { Upload, Download, RotateCcw, WandSparkles, Save } from "@lucide/vue";
import { Upload, Download, RotateCcw, WandSparkles, Save, Copy } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/composables/useToast";
import { copyToClipboard } from "@/lib/clipboard";
import {
DEFAULT_SQL_FORMATTER_SETTINGS,
SQL_FORMATTER_CONFIG_FORMATTER,
SQL_FORMATTER_CONFIG_VERSION,
normalizeSqlFormatterEditorSettings,
normalizeSqlFormatterSettings,
parseSqlFormatterConfig,
serializeSqlFormatterConfig,
sqlFormatterPlatformFromNavigator,
syncSqlFormatterConfigDraft,
validateSqlFormatterEditorSettings,
type SqlFormatterCase,
type SqlFormatterEditorShortcut,
type SqlFormatterEditorShortcutId,
type SqlFormatterExpressionWidth,
type SqlFormatterIndentStyle,
type SqlFormatterLinesBetweenQueries,
type SqlFormatterLogicalOperatorNewline,
type SqlFormatterOptionSettings,
type SqlFormatterParamTypes,
type SqlFormatterPlatform,
type SqlFormatterSettings,
type SqlFormatterTabWidth,
} from "@/lib/sqlFormatterConfig";
import { createSqlFormatterConfigKeymap, sqlFormatterConfigShortcutRows } from "@/lib/sqlFormatterConfigEditor";
import { createSqlFormatterConfigKeymap, sqlFormatterConfigShortcutLabelKey, sqlFormatterConfigShortcutRows } from "@/lib/sqlFormatterConfigEditor";
type EditorViewInstance = import("@codemirror/view").EditorView;
type CodeMirrorModules = {
@ -50,14 +63,21 @@ const jsonEditorRef = ref<HTMLDivElement>();
const jsonDraft = ref(serializeSqlFormatterConfig(props.modelValue));
const jsonValidationMessage = ref("");
const importError = ref("");
const advancedConfigError = ref("");
const editorShortcutError = ref("");
const jsonEditorLoading = ref(false);
const jsonEditorReady = ref(false);
const jsonEditorLoadError = ref("");
const paramTypesDraft = ref("");
const focusedAdvancedOption = ref<"paramTypes" | null>(null);
let cmView: EditorViewInstance | null = null;
let cmModules: CodeMirrorModules | null = null;
let keymapCompartment: import("@codemirror/state").Compartment | null = null;
let lastValidity: boolean | null = null;
const settings = computed(() => normalizeSqlFormatterSettings(props.modelValue));
const shortcutRows = computed(() => sqlFormatterConfigShortcutRows(globalThis.navigator?.platform || ""));
const shortcutRows = computed(() => sqlFormatterConfigShortcutRows(globalThis.navigator?.platform || "", settings.value.editor));
const caseOptions: { value: SqlFormatterCase; labelKey: string }[] = [
{ value: "upper", labelKey: "settings.sqlFormatterCaseUpper" },
@ -70,13 +90,29 @@ const logicalOperatorOptions: { value: SqlFormatterLogicalOperatorNewline; label
{ value: "after", labelKey: "settings.sqlFormatterLogicalAfter" },
];
const indentStyleOptions: { value: SqlFormatterIndentStyle; labelKey: string }[] = [
{ value: "standard", labelKey: "settings.sqlFormatterIndentStyleStandard" },
{ value: "tabularLeft", labelKey: "settings.sqlFormatterIndentStyleTabularLeft" },
{ value: "tabularRight", labelKey: "settings.sqlFormatterIndentStyleTabularRight" },
];
const shortcutPlatformOptions: { value: SqlFormatterPlatform; labelKey: string }[] = [
{ value: "windows", labelKey: "settings.sqlFormatterShortcutWindows" },
{ value: "linux", labelKey: "settings.sqlFormatterShortcutLinux" },
{ value: "macos", labelKey: "settings.sqlFormatterShortcutMacos" },
];
const currentShortcutPlatform = computed(() => sqlFormatterPlatformFromNavigator(globalThis.navigator?.platform || ""));
const currentShortcutPlatformOption = computed(() => shortcutPlatformOptions.find((option) => option.value === currentShortcutPlatform.value) ?? shortcutPlatformOptions[0]);
const tabWidthOptions: SqlFormatterTabWidth[] = [2, 4];
const expressionWidthOptions: SqlFormatterExpressionWidth[] = [50, 80, 120];
const linesBetweenQueriesOptions: SqlFormatterLinesBetweenQueries[] = [0, 1, 2];
const sqlFormatterOptionLabelKeys: Record<keyof SqlFormatterSettings, string> = {
const sqlFormatterOptionLabelKeys: Record<keyof SqlFormatterOptionSettings, string> = {
keywordCase: "settings.sqlFormatterKeywordCase",
dataTypeCase: "settings.sqlFormatterDataTypeCase",
functionCase: "settings.sqlFormatterFunctionCase",
identifierCase: "settings.sqlFormatterIdentifierCase",
indentStyle: "settings.sqlFormatterIndentStyle",
useTabs: "settings.sqlFormatterIndent",
tabWidth: "settings.sqlFormatterTabWidth",
logicalOperatorNewline: "settings.sqlFormatterLogicalOperatorNewline",
@ -84,13 +120,20 @@ const sqlFormatterOptionLabelKeys: Record<keyof SqlFormatterSettings, string> =
linesBetweenQueries: "settings.sqlFormatterLinesBetweenQueries",
denseOperators: "settings.sqlFormatterDenseOperators",
newlineBeforeSemicolon: "settings.sqlFormatterNewlineBeforeSemicolon",
paramTypes: "settings.sqlFormatterParamTypes",
};
const sqlFormatterConfigErrorKeys: Record<string, string> = {
"Invalid JSON.": "settings.sqlFormatterConfigErrorInvalidJson",
"Config must be a JSON object.": "settings.sqlFormatterConfigErrorObject",
"Unsupported config version.": "settings.sqlFormatterConfigErrorVersion",
"Unsupported formatter.": "settings.sqlFormatterConfigErrorFormatter",
"Unsupported formatter option: params.": "settings.sqlFormatterConfigErrorUnsupportedParams",
"Config options must be a JSON object.": "settings.sqlFormatterConfigErrorOptionsObject",
"Config editor must be a JSON object.": "settings.sqlFormatterConfigErrorEditorObject",
"Unsupported editor scope.": "settings.sqlFormatterConfigErrorEditorScope",
"Invalid editor platforms.": "settings.sqlFormatterConfigErrorEditorPlatforms",
"Config editor shortcuts must be an array.": "settings.sqlFormatterConfigErrorEditorShortcutsArray",
"Invalid editor shortcut value.": "settings.sqlFormatterConfigErrorEditorShortcutValue",
};
function emitValidity(value: boolean) {
@ -118,12 +161,27 @@ function localizeSqlFormatterConfigError(message: string): string {
const invalidOption = message.match(/^Invalid formatter option value: (.+)\.$/);
if (invalidOption?.[1]) {
const labelKey = sqlFormatterOptionLabelKeys[invalidOption[1] as keyof SqlFormatterSettings];
const labelKey = sqlFormatterOptionLabelKeys[invalidOption[1] as keyof SqlFormatterOptionSettings];
if (labelKey) {
return t("settings.sqlFormatterConfigErrorInvalidOptionValue", { option: t(labelKey) });
}
}
const unknownShortcut = message.match(/^Unknown editor shortcut: (.+)\.$/);
if (unknownShortcut?.[1]) {
return t("settings.sqlFormatterConfigErrorUnknownEditorShortcut", { shortcut: unknownShortcut[1] });
}
const invalidShortcut = message.match(/^Invalid editor shortcut value: (.+)\.$/);
if (invalidShortcut?.[1]) {
return t("settings.sqlFormatterConfigErrorInvalidEditorShortcut", { shortcut: invalidShortcut[1] });
}
const duplicateShortcut = message.match(/^Duplicate editor shortcut: (.+)\.$/);
if (duplicateShortcut?.[1]) {
return t("settings.sqlFormatterConfigErrorDuplicateEditorShortcut", { shortcut: duplicateShortcut[1] });
}
return t("settings.sqlFormatterConfigErrorInvalidConfig");
}
@ -144,9 +202,17 @@ function syncJsonDraft(text = jsonDraft.value): boolean {
function updateSettings(next: unknown) {
importError.value = "";
advancedConfigError.value = "";
emit("update:modelValue", normalizeSqlFormatterSettings(next));
}
function validateEditorShortcuts(value = settings.value.editor): boolean {
const result = validateSqlFormatterEditorSettings(value);
editorShortcutError.value = result.ok ? "" : localizeSqlFormatterConfigError(result.message);
emitValidity(activeMode.value === "form" ? result.ok : !jsonValidationMessage.value);
return result.ok;
}
function updateOption<K extends keyof SqlFormatterSettings>(key: K, value: SqlFormatterSettings[K]) {
updateSettings({ ...settings.value, [key]: value });
}
@ -155,6 +221,14 @@ function onCaseOption(key: "keywordCase" | "functionCase" | "dataTypeCase", valu
if (value === "upper" || value === "lower" || value === "preserve") updateOption(key, value);
}
function onIdentifierCase(value: any) {
if (value === "upper" || value === "lower" || value === "preserve") updateOption("identifierCase", value);
}
function onIndentStyle(value: any) {
if (value === "standard" || value === "tabularLeft" || value === "tabularRight") updateOption("indentStyle", value);
}
function onLogicalOperatorNewline(value: any) {
if (value === "before" || value === "after") updateOption("logicalOperatorNewline", value);
}
@ -178,6 +252,93 @@ function restoreDefaults() {
updateSettings(DEFAULT_SQL_FORMATTER_SETTINGS);
}
function stringifyAdvancedOption(value: SqlFormatterParamTypes | null): string {
return JSON.stringify(value, null, 2);
}
function setAdvancedDraft(value: SqlFormatterParamTypes | null) {
paramTypesDraft.value = stringifyAdvancedOption(value);
}
function onAdvancedJsonInput(event: Event) {
paramTypesDraft.value = (event.target as HTMLTextAreaElement).value;
}
function onJsonDraftTextareaInput(event: Event) {
const text = (event.target as HTMLTextAreaElement).value;
jsonDraft.value = text;
syncJsonDraft(text);
}
function onAdvancedJsonFocus() {
focusedAdvancedOption.value = "paramTypes";
}
function onAdvancedJsonBlur() {
focusedAdvancedOption.value = null;
applyAdvancedJsonOption();
}
function applyAdvancedJsonOption() {
let parsed: unknown = null;
const draft = paramTypesDraft.value.trim();
if (draft) {
try {
parsed = JSON.parse(draft);
} catch {
advancedConfigError.value = localizeSqlFormatterConfigError("Invalid JSON.");
return;
}
}
const result = parseSqlFormatterConfig(
JSON.stringify({
version: SQL_FORMATTER_CONFIG_VERSION,
formatter: SQL_FORMATTER_CONFIG_FORMATTER,
options: { paramTypes: parsed },
}),
);
if (!result.ok) {
advancedConfigError.value = localizeSqlFormatterConfigError(result.message);
return;
}
updateOption("paramTypes", result.settings.paramTypes);
setAdvancedDraft(result.settings.paramTypes);
advancedConfigError.value = "";
}
function updateEditorShortcut(id: SqlFormatterEditorShortcutId, updater: (shortcut: SqlFormatterEditorShortcut) => SqlFormatterEditorShortcut) {
const editor = normalizeSqlFormatterEditorSettings(settings.value.editor);
const next = {
...settings.value,
editor: {
...editor,
shortcuts: editor.shortcuts.map((shortcut) => (shortcut.id === id ? updater({ ...shortcut, keys: { ...shortcut.keys } }) : shortcut)),
},
};
updateSettings(next);
validateEditorShortcuts(next.editor);
}
function updateEditorShortcutEnabled(id: SqlFormatterEditorShortcutId, enabled: boolean) {
updateEditorShortcut(id, (shortcut) => ({ ...shortcut, enabled }));
}
function updateEditorShortcutKey(id: SqlFormatterEditorShortcutId, platform: SqlFormatterPlatform, value: string) {
updateEditorShortcut(id, (shortcut) => ({
...shortcut,
keys: {
...shortcut.keys,
[platform]: value,
},
}));
}
function shortcutLabelKey(id: SqlFormatterEditorShortcutId): string {
return sqlFormatterConfigShortcutLabelKey(id);
}
function importConfig() {
importError.value = "";
fileInputRef.value?.click();
@ -215,6 +376,15 @@ function exportConfig() {
URL.revokeObjectURL(url);
}
async function copyJsonDraft() {
try {
await copyToClipboard(cmView?.state.doc.toString() ?? jsonDraft.value);
toast(t("settings.sqlFormatterCopyJsonSuccess"));
} catch (e: any) {
toast(t("settings.sqlFormatterCopyJsonFailed", { message: e?.message || String(e) }), 3000);
}
}
function applyJsonDraft(): boolean {
const result = parseSqlFormatterConfig(jsonDraft.value);
if (!result.ok) {
@ -255,37 +425,59 @@ async function loadCodeMirrorModules(): Promise<CodeMirrorModules> {
function destroyJsonEditor() {
cmView?.destroy();
cmView = null;
keymapCompartment = null;
jsonEditorReady.value = false;
jsonEditorLoadError.value = "";
}
function jsonEditorKeymapExtension(modules: CodeMirrorModules) {
const { keymap } = modules.view;
const commands = modules.commands;
const search = modules.search;
const customKeymap = createSqlFormatterConfigKeymap(
{
indentMore: commands.indentMore,
indentLess: commands.indentLess,
copyLineDown: commands.copyLineDown,
copyLineUp: commands.copyLineUp,
deleteLine: commands.deleteLine,
moveLineUp: commands.moveLineUp,
moveLineDown: commands.moveLineDown,
undo: commands.undo,
redo: commands.redo,
selectAll: commands.selectAll,
openSearchPanel: search.openSearchPanel,
},
{
apply: applyJsonDraft,
formatJson: formatJsonDraft,
},
settings.value.editor,
);
return keymap.of([...customKeymap, ...search.searchKeymap, ...commands.historyKeymap, ...commands.defaultKeymap]);
}
function reconfigureJsonEditorKeymap() {
if (!cmView || !cmModules || !keymapCompartment) return;
cmView.dispatch({
effects: keymapCompartment.reconfigure(jsonEditorKeymapExtension(cmModules)),
});
}
async function initJsonEditor() {
if (cmView || !jsonEditorRef.value) return;
jsonEditorLoading.value = true;
jsonEditorLoadError.value = "";
try {
const modules = await loadCodeMirrorModules();
if (activeMode.value !== "json" || cmView || !jsonEditorRef.value) return;
const { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } = modules.view;
const { EditorState } = modules.state;
const { EditorView, lineNumbers, highlightActiveLine, highlightActiveLineGutter } = modules.view;
const { EditorState, Compartment } = modules.state;
const { json } = modules.langJson;
const commands = modules.commands;
const search = modules.search;
const customKeymap = createSqlFormatterConfigKeymap(
{
indentMore: commands.indentMore,
indentLess: commands.indentLess,
copyLineDown: commands.copyLineDown,
copyLineUp: commands.copyLineUp,
deleteLine: commands.deleteLine,
moveLineUp: commands.moveLineUp,
moveLineDown: commands.moveLineDown,
openSearchPanel: search.openSearchPanel,
},
{
apply: applyJsonDraft,
formatJson: formatJsonDraft,
},
);
keymapCompartment = new Compartment();
const state = EditorState.create({
doc: jsonDraft.value,
@ -296,7 +488,7 @@ async function initJsonEditor() {
commands.history(),
search.search({ top: true }),
json(),
keymap.of([...customKeymap, ...search.searchKeymap, ...commands.historyKeymap, ...commands.defaultKeymap]),
keymapCompartment.of(jsonEditorKeymapExtension(modules)),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (!update.docChanged) return;
@ -333,11 +525,32 @@ async function initJsonEditor() {
});
cmView = new EditorView({ state, parent: jsonEditorRef.value });
jsonEditorReady.value = true;
} catch (e: any) {
jsonEditorReady.value = false;
jsonEditorLoadError.value = e?.message || String(e);
} finally {
jsonEditorLoading.value = false;
}
}
watch(
() => settings.value.paramTypes,
(value) => {
if (focusedAdvancedOption.value !== "paramTypes") paramTypesDraft.value = stringifyAdvancedOption(value);
},
{ deep: true, immediate: true },
);
watch(
() => settings.value.editor,
(value) => {
validateEditorShortcuts(value);
if (activeMode.value === "json") reconfigureJsonEditorKeymap();
},
{ deep: true, immediate: true },
);
watch(
() => props.modelValue,
(value) => {
@ -365,7 +578,7 @@ watch(
await initJsonEditor();
return;
}
emitValidity(true);
validateEditorShortcuts();
destroyJsonEditor();
},
{ immediate: true },
@ -405,7 +618,7 @@ onBeforeUnmount(() => {
</TabsList>
<TabsContent value="form" class="m-0 flex flex-col gap-4 pt-2">
<div class="grid gap-4 md:grid-cols-3">
<div class="grid gap-4 md:grid-cols-4">
<div class="space-y-2">
<Label>{{ t("settings.sqlFormatterKeywordCase") }}</Label>
<Select :model-value="settings.keywordCase" @update:model-value="(value: any) => onCaseOption('keywordCase', value)">
@ -447,9 +660,23 @@ onBeforeUnmount(() => {
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{{ t("settings.sqlFormatterIdentifierCase") }}</Label>
<Select :model-value="settings.identifierCase" @update:model-value="onIdentifierCase">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in caseOptions" :key="option.value" :value="option.value">
{{ t(option.labelKey) }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_10rem]">
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_10rem_12rem]">
<div class="space-y-2">
<Label>{{ t("settings.sqlFormatterIndent") }}</Label>
<div class="grid grid-cols-2 gap-2">
@ -475,6 +702,20 @@ onBeforeUnmount(() => {
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{{ t("settings.sqlFormatterIndentStyle") }}</Label>
<Select :model-value="settings.indentStyle" @update:model-value="onIndentStyle">
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in indentStyleOptions" :key="option.value" :value="option.value">
{{ t(option.labelKey) }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="grid gap-4 md:grid-cols-3">
@ -534,6 +775,46 @@ onBeforeUnmount(() => {
<Switch id="sql-formatter-newline-before-semicolon" :model-value="settings.newlineBeforeSemicolon" @update:model-value="(value: boolean) => updateOption('newlineBeforeSemicolon', value)" />
</div>
</div>
<div class="space-y-3 rounded-md border border-border/70 bg-muted/10 p-3">
<div class="text-sm font-medium">{{ t("settings.sqlFormatterAdvancedOptions") }}</div>
<div class="space-y-2">
<Label for="sql-formatter-param-types">{{ t("settings.sqlFormatterParamTypes") }}</Label>
<textarea
id="sql-formatter-param-types"
:value="paramTypesDraft"
spellcheck="false"
class="min-h-28 w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40"
@input="onAdvancedJsonInput"
@focus="onAdvancedJsonFocus"
@blur="onAdvancedJsonBlur"
/>
</div>
<p v-if="advancedConfigError" class="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{{ advancedConfigError }}
</p>
</div>
<div class="space-y-3 rounded-md border border-border/70 bg-muted/10 p-3">
<div class="text-sm font-medium">{{ t("settings.sqlFormatterEditorShortcuts") }}</div>
<div class="overflow-x-auto rounded-md border bg-background">
<div class="grid min-w-[420px] grid-cols-[minmax(10rem,1fr)_5rem_minmax(12rem,1.2fr)] items-center gap-2 border-b bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground">
<span>{{ t("settings.sqlFormatterShortcutAction") }}</span>
<span class="text-center">{{ t("settings.sqlFormatterShortcutEnabled") }}</span>
<span>{{ t(currentShortcutPlatformOption.labelKey) }}</span>
</div>
<div v-for="shortcut in settings.editor.shortcuts" :key="shortcut.id" class="grid min-w-[420px] grid-cols-[minmax(10rem,1fr)_5rem_minmax(12rem,1.2fr)] items-center gap-2 border-b px-3 py-2 text-xs last:border-b-0">
<span class="min-w-0 truncate text-muted-foreground">{{ t(shortcutLabelKey(shortcut.id)) }}</span>
<div class="flex justify-center">
<Switch :model-value="shortcut.enabled" @update:model-value="(value: boolean) => updateEditorShortcutEnabled(shortcut.id, value)" />
</div>
<Input :model-value="shortcut.keys[currentShortcutPlatform]" class="h-8 font-mono text-xs" :disabled="!shortcut.enabled" @update:model-value="(value: string | number) => updateEditorShortcutKey(shortcut.id, currentShortcutPlatform, String(value))" />
</div>
</div>
<p v-if="editorShortcutError" class="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{{ editorShortcutError }}
</p>
</div>
</TabsContent>
<TabsContent value="json" class="m-0 flex min-h-0 flex-col gap-3 pt-2">
@ -542,6 +823,10 @@ onBeforeUnmount(() => {
<WandSparkles class="mr-2 h-4 w-4" />
{{ t("settings.sqlFormatterShortcutFormatJson") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="copyJsonDraft">
<Copy class="mr-2 h-4 w-4" />
{{ t("settings.sqlFormatterCopyJson") }}
</Button>
<Button type="button" size="sm" :disabled="!!jsonValidationMessage" @click="applyJsonDraft">
<Save class="mr-2 h-4 w-4" />
{{ t("settings.sqlFormatterShortcutApply") }}
@ -549,7 +834,19 @@ onBeforeUnmount(() => {
<span v-if="jsonEditorLoading" class="text-xs text-muted-foreground">{{ t("common.loading") }}</span>
</div>
<div ref="jsonEditorRef" class="min-h-[260px]" />
<div ref="jsonEditorRef" v-show="jsonEditorReady || jsonEditorLoading" class="min-h-[320px]" />
<textarea
v-if="!jsonEditorReady && !jsonEditorLoading"
:value="jsonDraft"
spellcheck="false"
class="min-h-[320px] w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/40"
@input="onJsonDraftTextareaInput"
/>
<p v-if="jsonEditorLoadError" class="rounded-md border border-amber-300/60 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-400/40 dark:bg-amber-950/30 dark:text-amber-200">
{{ t("settings.sqlFormatterJsonEditorLoadFailed", { message: jsonEditorLoadError }) }}
</p>
<p v-if="jsonValidationMessage" class="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{{ jsonValidationMessage }}

View File

@ -1731,12 +1731,17 @@
sqlFormatterImport: "Import config",
sqlFormatterExport: "Export config",
sqlFormatterImportSuccess: "Formatter config imported.",
sqlFormatterCopyJson: "Copy JSON",
sqlFormatterCopyJsonSuccess: "JSON copied.",
sqlFormatterCopyJsonFailed: "Copy failed: {message}",
sqlFormatterJsonEditorLoadFailed: "JSON editor failed to load. Switched to a basic input: {message}",
sqlFormatterRestoreDefaults: "Restore defaults",
sqlFormatterFormMode: "Form",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "Keyword case",
sqlFormatterFunctionCase: "Function case",
sqlFormatterDataTypeCase: "Data type case",
sqlFormatterIdentifierCase: "Identifier case",
sqlFormatterCaseUpper: "Uppercase",
sqlFormatterCaseLower: "Lowercase",
sqlFormatterCasePreserve: "Preserve",
@ -1744,6 +1749,10 @@
sqlFormatterIndentSpaces: "Spaces",
sqlFormatterIndentTabs: "Tabs",
sqlFormatterTabWidth: "Tab width",
sqlFormatterIndentStyle: "Indent style",
sqlFormatterIndentStyleStandard: "Standard",
sqlFormatterIndentStyleTabularLeft: "Tabular left",
sqlFormatterIndentStyleTabularRight: "Tabular right",
sqlFormatterLogicalOperatorNewline: "Logical operator newline",
sqlFormatterLogicalBefore: "Before operator",
sqlFormatterLogicalAfter: "After operator",
@ -1751,6 +1760,15 @@
sqlFormatterLinesBetweenQueries: "Lines between queries",
sqlFormatterDenseOperators: "Dense operators",
sqlFormatterNewlineBeforeSemicolon: "Semicolon on new line",
sqlFormatterAdvancedOptions: "Advanced options",
sqlFormatterParams: "Params",
sqlFormatterParamTypes: "Param types",
sqlFormatterEditorShortcuts: "JSON editor shortcuts",
sqlFormatterShortcutAction: "Action",
sqlFormatterShortcutEnabled: "Enabled",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "Find",
sqlFormatterShortcutReplace: "Replace",
sqlFormatterShortcutIndentMore: "Indent more",
@ -1758,16 +1776,32 @@
sqlFormatterShortcutDuplicateLine: "Duplicate current line",
sqlFormatterShortcutDeleteLine: "Delete current line",
sqlFormatterShortcutMoveLine: "Move line",
sqlFormatterShortcutMoveLineUp: "Move line up",
sqlFormatterShortcutMoveLineDown: "Move line down",
sqlFormatterShortcutCopyLine: "Copy line",
sqlFormatterShortcutCopyLineUp: "Copy line up",
sqlFormatterShortcutCopyLineDown: "Copy line down",
sqlFormatterShortcutUndo: "Undo",
sqlFormatterShortcutRedo: "Redo",
sqlFormatterShortcutSelectAll: "Select all",
sqlFormatterShortcutFormatJson: "Format JSON",
sqlFormatterShortcutApply: "Apply config",
sqlFormatterConfigErrorInvalidJson: "Invalid JSON.",
sqlFormatterConfigErrorObject: "Config must be a JSON object.",
sqlFormatterConfigErrorVersion: "Unsupported config version.",
sqlFormatterConfigErrorFormatter: "Unsupported formatter.",
sqlFormatterConfigErrorUnsupportedParams: "The params replacement option is not supported. SQL formatting will not replace placeholders.",
sqlFormatterConfigErrorOptionsObject: "Config options must be a JSON object.",
sqlFormatterConfigErrorEditorObject: "Editor config must be a JSON object.",
sqlFormatterConfigErrorEditorScope: "Unsupported editor scope.",
sqlFormatterConfigErrorEditorPlatforms: "Invalid editor platforms.",
sqlFormatterConfigErrorEditorShortcutsArray: "Editor shortcuts must be an array.",
sqlFormatterConfigErrorEditorShortcutValue: "Invalid editor shortcut.",
sqlFormatterConfigErrorUnknownOption: "Unknown formatter option: {option}.",
sqlFormatterConfigErrorInvalidOptionValue: "Invalid value for {option}.",
sqlFormatterConfigErrorUnknownEditorShortcut: "Unknown editor shortcut: {shortcut}.",
sqlFormatterConfigErrorInvalidEditorShortcut: "Invalid editor shortcut: {shortcut}.",
sqlFormatterConfigErrorDuplicateEditorShortcut: "Duplicate editor shortcut: {shortcut}.",
sqlFormatterConfigErrorInvalidConfig: "Invalid formatter config.",
appearanceTab: "Appearance",
navigationTab: "Navigation",

View File

@ -1510,12 +1510,17 @@
sqlFormatterImport: "Importar configuración",
sqlFormatterExport: "Exportar configuración",
sqlFormatterImportSuccess: "Configuración del formateador importada.",
sqlFormatterCopyJson: "Copiar JSON",
sqlFormatterCopyJsonSuccess: "JSON copiado.",
sqlFormatterCopyJsonFailed: "Error al copiar: {message}",
sqlFormatterJsonEditorLoadFailed: "No se pudo cargar el editor JSON. Se cambió a una entrada básica: {message}",
sqlFormatterRestoreDefaults: "Restaurar valores por defecto",
sqlFormatterFormMode: "Formulario",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "Mayúsculas/minúsculas de palabras clave",
sqlFormatterFunctionCase: "Mayúsculas/minúsculas de funciones",
sqlFormatterDataTypeCase: "Mayúsculas/minúsculas de tipos de datos",
sqlFormatterIdentifierCase: "Mayúsculas/minúsculas de identificadores",
sqlFormatterCaseUpper: "Mayúsculas",
sqlFormatterCaseLower: "Minúsculas",
sqlFormatterCasePreserve: "Conservar",
@ -1523,6 +1528,10 @@
sqlFormatterIndentSpaces: "Espacios",
sqlFormatterIndentTabs: "Tabulaciones",
sqlFormatterTabWidth: "Ancho de tabulación",
sqlFormatterIndentStyle: "Estilo de sangría",
sqlFormatterIndentStyleStandard: "Estándar",
sqlFormatterIndentStyleTabularLeft: "Tabular izquierda",
sqlFormatterIndentStyleTabularRight: "Tabular derecha",
sqlFormatterLogicalOperatorNewline: "Salto de línea en operador lógico",
sqlFormatterLogicalBefore: "Antes del operador",
sqlFormatterLogicalAfter: "Después del operador",
@ -1530,6 +1539,15 @@
sqlFormatterLinesBetweenQueries: "Líneas entre consultas",
sqlFormatterDenseOperators: "Operadores compactos",
sqlFormatterNewlineBeforeSemicolon: "Punto y coma en nueva línea",
sqlFormatterAdvancedOptions: "Opciones avanzadas",
sqlFormatterParams: "Parámetros",
sqlFormatterParamTypes: "Tipos de parámetro",
sqlFormatterEditorShortcuts: "Atajos del editor JSON",
sqlFormatterShortcutAction: "Acción",
sqlFormatterShortcutEnabled: "Activo",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "Buscar",
sqlFormatterShortcutReplace: "Reemplazar",
sqlFormatterShortcutIndentMore: "Aumentar sangría",
@ -1537,16 +1555,32 @@
sqlFormatterShortcutDuplicateLine: "Duplicar línea actual",
sqlFormatterShortcutDeleteLine: "Eliminar línea actual",
sqlFormatterShortcutMoveLine: "Mover línea",
sqlFormatterShortcutMoveLineUp: "Mover línea arriba",
sqlFormatterShortcutMoveLineDown: "Mover línea abajo",
sqlFormatterShortcutCopyLine: "Copiar línea",
sqlFormatterShortcutCopyLineUp: "Copiar línea arriba",
sqlFormatterShortcutCopyLineDown: "Copiar línea abajo",
sqlFormatterShortcutUndo: "Deshacer",
sqlFormatterShortcutRedo: "Rehacer",
sqlFormatterShortcutSelectAll: "Seleccionar todo",
sqlFormatterShortcutFormatJson: "Formatear JSON",
sqlFormatterShortcutApply: "Aplicar configuración",
sqlFormatterConfigErrorInvalidJson: "JSON no válido.",
sqlFormatterConfigErrorObject: "La configuración debe ser un objeto JSON.",
sqlFormatterConfigErrorVersion: "Versión de configuración no compatible.",
sqlFormatterConfigErrorFormatter: "Formateador no compatible.",
sqlFormatterConfigErrorUnsupportedParams: "La sustitución params no es compatible. El formateo SQL no reemplazará marcadores.",
sqlFormatterConfigErrorOptionsObject: "Las opciones de configuración deben ser un objeto JSON.",
sqlFormatterConfigErrorEditorObject: "La configuración del editor debe ser un objeto JSON.",
sqlFormatterConfigErrorEditorScope: "Ámbito de editor no compatible.",
sqlFormatterConfigErrorEditorPlatforms: "Plataformas de editor no válidas.",
sqlFormatterConfigErrorEditorShortcutsArray: "Los atajos del editor deben ser un array.",
sqlFormatterConfigErrorEditorShortcutValue: "Atajo de editor no válido.",
sqlFormatterConfigErrorUnknownOption: "Opción desconocida del formateador: {option}.",
sqlFormatterConfigErrorInvalidOptionValue: "Valor no válido para {option}.",
sqlFormatterConfigErrorUnknownEditorShortcut: "Atajo de editor desconocido: {shortcut}.",
sqlFormatterConfigErrorInvalidEditorShortcut: "Atajo de editor no válido: {shortcut}.",
sqlFormatterConfigErrorDuplicateEditorShortcut: "Atajo de editor duplicado: {shortcut}.",
sqlFormatterConfigErrorInvalidConfig: "Configuración del formateador no válida.",
appearanceTab: "Apariencia",
navigationTab: "Navegación",

View File

@ -1627,12 +1627,17 @@
sqlFormatterImport: "Importa configurazione",
sqlFormatterExport: "Esporta configurazione",
sqlFormatterImportSuccess: "Configurazione del formattatore importata.",
sqlFormatterCopyJson: "Copia JSON",
sqlFormatterCopyJsonSuccess: "JSON copiato.",
sqlFormatterCopyJsonFailed: "Copia non riuscita: {message}",
sqlFormatterJsonEditorLoadFailed: "Impossibile caricare l'editor JSON. Uso di un input di base: {message}",
sqlFormatterRestoreDefaults: "Ripristina predefiniti",
sqlFormatterFormMode: "Modulo",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "Maiuscole/minuscole parole chiave",
sqlFormatterFunctionCase: "Maiuscole/minuscole funzioni",
sqlFormatterDataTypeCase: "Maiuscole/minuscole tipi di dati",
sqlFormatterIdentifierCase: "Maiuscole/minuscole identificatori",
sqlFormatterCaseUpper: "Maiuscolo",
sqlFormatterCaseLower: "Minuscolo",
sqlFormatterCasePreserve: "Mantieni",
@ -1640,6 +1645,10 @@
sqlFormatterIndentSpaces: "Spazi",
sqlFormatterIndentTabs: "Tabulazioni",
sqlFormatterTabWidth: "Larghezza tab",
sqlFormatterIndentStyle: "Stile rientro",
sqlFormatterIndentStyleStandard: "Standard",
sqlFormatterIndentStyleTabularLeft: "Tabellare sinistra",
sqlFormatterIndentStyleTabularRight: "Tabellare destra",
sqlFormatterLogicalOperatorNewline: "Nuova riga operatore logico",
sqlFormatterLogicalBefore: "Prima dell'operatore",
sqlFormatterLogicalAfter: "Dopo l'operatore",
@ -1647,6 +1656,15 @@
sqlFormatterLinesBetweenQueries: "Righe tra query",
sqlFormatterDenseOperators: "Operatori compatti",
sqlFormatterNewlineBeforeSemicolon: "Punto e virgola su nuova riga",
sqlFormatterAdvancedOptions: "Opzioni avanzate",
sqlFormatterParams: "Parametri",
sqlFormatterParamTypes: "Tipi parametro",
sqlFormatterEditorShortcuts: "Scorciatoie editor JSON",
sqlFormatterShortcutAction: "Azione",
sqlFormatterShortcutEnabled: "Attiva",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "Trova",
sqlFormatterShortcutReplace: "Sostituisci",
sqlFormatterShortcutIndentMore: "Aumenta rientro",
@ -1654,16 +1672,32 @@
sqlFormatterShortcutDuplicateLine: "Duplica riga corrente",
sqlFormatterShortcutDeleteLine: "Elimina riga corrente",
sqlFormatterShortcutMoveLine: "Sposta riga",
sqlFormatterShortcutMoveLineUp: "Sposta riga su",
sqlFormatterShortcutMoveLineDown: "Sposta riga giù",
sqlFormatterShortcutCopyLine: "Copia riga",
sqlFormatterShortcutCopyLineUp: "Copia riga su",
sqlFormatterShortcutCopyLineDown: "Copia riga giù",
sqlFormatterShortcutUndo: "Annulla",
sqlFormatterShortcutRedo: "Ripeti",
sqlFormatterShortcutSelectAll: "Seleziona tutto",
sqlFormatterShortcutFormatJson: "Formatta JSON",
sqlFormatterShortcutApply: "Applica configurazione",
sqlFormatterConfigErrorInvalidJson: "JSON non valido.",
sqlFormatterConfigErrorObject: "La configurazione deve essere un oggetto JSON.",
sqlFormatterConfigErrorVersion: "Versione configurazione non supportata.",
sqlFormatterConfigErrorFormatter: "Formattatore non supportato.",
sqlFormatterConfigErrorUnsupportedParams: "La sostituzione params non è supportata. La formattazione SQL non sostituirà i segnaposto.",
sqlFormatterConfigErrorOptionsObject: "Le opzioni di configurazione devono essere un oggetto JSON.",
sqlFormatterConfigErrorEditorObject: "La configurazione dell'editor deve essere un oggetto JSON.",
sqlFormatterConfigErrorEditorScope: "Ambito editor non supportato.",
sqlFormatterConfigErrorEditorPlatforms: "Piattaforme editor non valide.",
sqlFormatterConfigErrorEditorShortcutsArray: "Le scorciatoie editor devono essere un array.",
sqlFormatterConfigErrorEditorShortcutValue: "Scorciatoia editor non valida.",
sqlFormatterConfigErrorUnknownOption: "Opzione del formattatore sconosciuta: {option}.",
sqlFormatterConfigErrorInvalidOptionValue: "Valore non valido per {option}.",
sqlFormatterConfigErrorUnknownEditorShortcut: "Scorciatoia editor sconosciuta: {shortcut}.",
sqlFormatterConfigErrorInvalidEditorShortcut: "Scorciatoia editor non valida: {shortcut}.",
sqlFormatterConfigErrorDuplicateEditorShortcut: "Scorciatoia editor duplicata: {shortcut}.",
sqlFormatterConfigErrorInvalidConfig: "Configurazione del formattatore non valida.",
appearanceTab: "Aspetto",
navigationTab: "Navigazione",

View File

@ -1627,12 +1627,17 @@
sqlFormatterImport: "Importar configuração",
sqlFormatterExport: "Exportar configuração",
sqlFormatterImportSuccess: "Configuração do formatador importada.",
sqlFormatterCopyJson: "Copiar JSON",
sqlFormatterCopyJsonSuccess: "JSON copiado.",
sqlFormatterCopyJsonFailed: "Falha ao copiar: {message}",
sqlFormatterJsonEditorLoadFailed: "Falha ao carregar o editor JSON. Alternado para uma entrada básica: {message}",
sqlFormatterRestoreDefaults: "Restaurar padrões",
sqlFormatterFormMode: "Formulário",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "Maiúsculas/minúsculas de palavras-chave",
sqlFormatterFunctionCase: "Maiúsculas/minúsculas de funções",
sqlFormatterDataTypeCase: "Maiúsculas/minúsculas de tipos de dados",
sqlFormatterIdentifierCase: "Maiúsculas/minúsculas de identificadores",
sqlFormatterCaseUpper: "Maiúsculas",
sqlFormatterCaseLower: "Minúsculas",
sqlFormatterCasePreserve: "Preservar",
@ -1640,6 +1645,10 @@
sqlFormatterIndentSpaces: "Espaços",
sqlFormatterIndentTabs: "Tabulações",
sqlFormatterTabWidth: "Largura do tab",
sqlFormatterIndentStyle: "Estilo de recuo",
sqlFormatterIndentStyleStandard: "Padrão",
sqlFormatterIndentStyleTabularLeft: "Tabular esquerda",
sqlFormatterIndentStyleTabularRight: "Tabular direita",
sqlFormatterLogicalOperatorNewline: "Quebra de linha no operador lógico",
sqlFormatterLogicalBefore: "Antes do operador",
sqlFormatterLogicalAfter: "Depois do operador",
@ -1647,6 +1656,15 @@
sqlFormatterLinesBetweenQueries: "Linhas entre consultas",
sqlFormatterDenseOperators: "Operadores compactos",
sqlFormatterNewlineBeforeSemicolon: "Ponto e vírgula em nova linha",
sqlFormatterAdvancedOptions: "Opções avançadas",
sqlFormatterParams: "Parâmetros",
sqlFormatterParamTypes: "Tipos de parâmetro",
sqlFormatterEditorShortcuts: "Atalhos do editor JSON",
sqlFormatterShortcutAction: "Ação",
sqlFormatterShortcutEnabled: "Ativo",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "Localizar",
sqlFormatterShortcutReplace: "Substituir",
sqlFormatterShortcutIndentMore: "Aumentar recuo",
@ -1654,16 +1672,32 @@
sqlFormatterShortcutDuplicateLine: "Duplicar linha atual",
sqlFormatterShortcutDeleteLine: "Excluir linha atual",
sqlFormatterShortcutMoveLine: "Mover linha",
sqlFormatterShortcutMoveLineUp: "Mover linha para cima",
sqlFormatterShortcutMoveLineDown: "Mover linha para baixo",
sqlFormatterShortcutCopyLine: "Copiar linha",
sqlFormatterShortcutCopyLineUp: "Copiar linha para cima",
sqlFormatterShortcutCopyLineDown: "Copiar linha para baixo",
sqlFormatterShortcutUndo: "Desfazer",
sqlFormatterShortcutRedo: "Refazer",
sqlFormatterShortcutSelectAll: "Selecionar tudo",
sqlFormatterShortcutFormatJson: "Formatar JSON",
sqlFormatterShortcutApply: "Aplicar configuração",
sqlFormatterConfigErrorInvalidJson: "JSON inválido.",
sqlFormatterConfigErrorObject: "A configuração deve ser um objeto JSON.",
sqlFormatterConfigErrorVersion: "Versão de configuração não compatível.",
sqlFormatterConfigErrorFormatter: "Formatador não compatível.",
sqlFormatterConfigErrorUnsupportedParams: "A substituição params não é compatível. A formatação SQL não substituirá marcadores.",
sqlFormatterConfigErrorOptionsObject: "As opções de configuração devem ser um objeto JSON.",
sqlFormatterConfigErrorEditorObject: "A configuração do editor deve ser um objeto JSON.",
sqlFormatterConfigErrorEditorScope: "Escopo de editor não compatível.",
sqlFormatterConfigErrorEditorPlatforms: "Plataformas de editor inválidas.",
sqlFormatterConfigErrorEditorShortcutsArray: "Os atalhos do editor devem ser um array.",
sqlFormatterConfigErrorEditorShortcutValue: "Atalho de editor inválido.",
sqlFormatterConfigErrorUnknownOption: "Opção desconhecida do formatador: {option}.",
sqlFormatterConfigErrorInvalidOptionValue: "Valor inválido para {option}.",
sqlFormatterConfigErrorUnknownEditorShortcut: "Atalho de editor desconhecido: {shortcut}.",
sqlFormatterConfigErrorInvalidEditorShortcut: "Atalho de editor inválido: {shortcut}.",
sqlFormatterConfigErrorDuplicateEditorShortcut: "Atalho de editor duplicado: {shortcut}.",
sqlFormatterConfigErrorInvalidConfig: "Configuração do formatador inválida.",
appearanceTab: "Aparência",
navigationTab: "Navegação",

View File

@ -1730,12 +1730,17 @@
sqlFormatterImport: "导入配置",
sqlFormatterExport: "导出配置",
sqlFormatterImportSuccess: "格式化配置已导入。",
sqlFormatterCopyJson: "复制 JSON",
sqlFormatterCopyJsonSuccess: "JSON 已复制。",
sqlFormatterCopyJsonFailed: "复制失败:{message}",
sqlFormatterJsonEditorLoadFailed: "JSON 编辑器加载失败,已切换为基础输入框:{message}",
sqlFormatterRestoreDefaults: "恢复默认",
sqlFormatterFormMode: "表单",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "关键字大小写",
sqlFormatterFunctionCase: "函数名大小写",
sqlFormatterDataTypeCase: "数据类型大小写",
sqlFormatterIdentifierCase: "标识符大小写",
sqlFormatterCaseUpper: "大写",
sqlFormatterCaseLower: "小写",
sqlFormatterCasePreserve: "保持原样",
@ -1743,6 +1748,10 @@
sqlFormatterIndentSpaces: "空格",
sqlFormatterIndentTabs: "Tab",
sqlFormatterTabWidth: "缩进宽度",
sqlFormatterIndentStyle: "缩进风格",
sqlFormatterIndentStyleStandard: "标准",
sqlFormatterIndentStyleTabularLeft: "表格式左对齐",
sqlFormatterIndentStyleTabularRight: "表格式右对齐",
sqlFormatterLogicalOperatorNewline: "逻辑运算符换行",
sqlFormatterLogicalBefore: "运算符前换行",
sqlFormatterLogicalAfter: "运算符后换行",
@ -1750,6 +1759,15 @@
sqlFormatterLinesBetweenQueries: "查询之间空行",
sqlFormatterDenseOperators: "紧凑运算符",
sqlFormatterNewlineBeforeSemicolon: "分号单独换行",
sqlFormatterAdvancedOptions: "高级选项",
sqlFormatterParams: "参数替换",
sqlFormatterParamTypes: "参数类型",
sqlFormatterEditorShortcuts: "JSON 编辑器快捷键",
sqlFormatterShortcutAction: "功能",
sqlFormatterShortcutEnabled: "启用",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "查找",
sqlFormatterShortcutReplace: "替换",
sqlFormatterShortcutIndentMore: "增加缩进",
@ -1757,16 +1775,32 @@
sqlFormatterShortcutDuplicateLine: "复制当前行",
sqlFormatterShortcutDeleteLine: "删除当前行",
sqlFormatterShortcutMoveLine: "移动行",
sqlFormatterShortcutMoveLineUp: "当前行上移",
sqlFormatterShortcutMoveLineDown: "当前行下移",
sqlFormatterShortcutCopyLine: "复制行",
sqlFormatterShortcutCopyLineUp: "向上复制当前行",
sqlFormatterShortcutCopyLineDown: "向下复制当前行",
sqlFormatterShortcutUndo: "撤销",
sqlFormatterShortcutRedo: "重做",
sqlFormatterShortcutSelectAll: "全选",
sqlFormatterShortcutFormatJson: "格式化 JSON",
sqlFormatterShortcutApply: "应用配置",
sqlFormatterConfigErrorInvalidJson: "JSON 无效。",
sqlFormatterConfigErrorObject: "配置必须是 JSON 对象。",
sqlFormatterConfigErrorVersion: "不支持的配置版本。",
sqlFormatterConfigErrorFormatter: "不支持的格式化器。",
sqlFormatterConfigErrorUnsupportedParams: "不支持 params 参数替换。SQL 格式化不会替换占位符。",
sqlFormatterConfigErrorOptionsObject: "配置选项必须是 JSON 对象。",
sqlFormatterConfigErrorEditorObject: "编辑器配置必须是 JSON 对象。",
sqlFormatterConfigErrorEditorScope: "不支持的编辑器作用域。",
sqlFormatterConfigErrorEditorPlatforms: "编辑器平台配置无效。",
sqlFormatterConfigErrorEditorShortcutsArray: "编辑器快捷键必须是数组。",
sqlFormatterConfigErrorEditorShortcutValue: "编辑器快捷键无效。",
sqlFormatterConfigErrorUnknownOption: "未知格式化选项:{option}。",
sqlFormatterConfigErrorInvalidOptionValue: "“{option}”的值无效。",
sqlFormatterConfigErrorUnknownEditorShortcut: "未知编辑器快捷键:{shortcut}。",
sqlFormatterConfigErrorInvalidEditorShortcut: "编辑器快捷键无效:{shortcut}。",
sqlFormatterConfigErrorDuplicateEditorShortcut: "编辑器快捷键重复:{shortcut}。",
sqlFormatterConfigErrorInvalidConfig: "格式化配置无效。",
appearanceTab: "外观",
navigationTab: "导航",

View File

@ -1604,12 +1604,17 @@
sqlFormatterImport: "匯入設定",
sqlFormatterExport: "匯出設定",
sqlFormatterImportSuccess: "格式化設定已匯入。",
sqlFormatterCopyJson: "複製 JSON",
sqlFormatterCopyJsonSuccess: "JSON 已複製。",
sqlFormatterCopyJsonFailed: "複製失敗:{message}",
sqlFormatterJsonEditorLoadFailed: "JSON 編輯器載入失敗,已切換為基本輸入框:{message}",
sqlFormatterRestoreDefaults: "還原預設",
sqlFormatterFormMode: "表單",
sqlFormatterJsonMode: "JSON",
sqlFormatterKeywordCase: "關鍵字大小寫",
sqlFormatterFunctionCase: "函式名稱大小寫",
sqlFormatterDataTypeCase: "資料型別大小寫",
sqlFormatterIdentifierCase: "識別符大小寫",
sqlFormatterCaseUpper: "大寫",
sqlFormatterCaseLower: "小寫",
sqlFormatterCasePreserve: "保持原樣",
@ -1617,6 +1622,10 @@
sqlFormatterIndentSpaces: "空格",
sqlFormatterIndentTabs: "Tab",
sqlFormatterTabWidth: "縮排寬度",
sqlFormatterIndentStyle: "縮排風格",
sqlFormatterIndentStyleStandard: "標準",
sqlFormatterIndentStyleTabularLeft: "表格式靠左",
sqlFormatterIndentStyleTabularRight: "表格式靠右",
sqlFormatterLogicalOperatorNewline: "邏輯運算子換行",
sqlFormatterLogicalBefore: "運算子前換行",
sqlFormatterLogicalAfter: "運算子後換行",
@ -1624,6 +1633,15 @@
sqlFormatterLinesBetweenQueries: "查詢之間空行",
sqlFormatterDenseOperators: "緊湊運算子",
sqlFormatterNewlineBeforeSemicolon: "分號獨立換行",
sqlFormatterAdvancedOptions: "進階選項",
sqlFormatterParams: "參數替換",
sqlFormatterParamTypes: "參數類型",
sqlFormatterEditorShortcuts: "JSON 編輯器快速鍵",
sqlFormatterShortcutAction: "功能",
sqlFormatterShortcutEnabled: "啟用",
sqlFormatterShortcutWindows: "Windows",
sqlFormatterShortcutLinux: "Linux",
sqlFormatterShortcutMacos: "macOS",
sqlFormatterShortcutFind: "尋找",
sqlFormatterShortcutReplace: "取代",
sqlFormatterShortcutIndentMore: "增加縮排",
@ -1631,16 +1649,32 @@
sqlFormatterShortcutDuplicateLine: "複製目前行",
sqlFormatterShortcutDeleteLine: "刪除目前行",
sqlFormatterShortcutMoveLine: "移動行",
sqlFormatterShortcutMoveLineUp: "目前行上移",
sqlFormatterShortcutMoveLineDown: "目前行下移",
sqlFormatterShortcutCopyLine: "複製行",
sqlFormatterShortcutCopyLineUp: "向上複製目前行",
sqlFormatterShortcutCopyLineDown: "向下複製目前行",
sqlFormatterShortcutUndo: "復原",
sqlFormatterShortcutRedo: "重做",
sqlFormatterShortcutSelectAll: "全選",
sqlFormatterShortcutFormatJson: "格式化 JSON",
sqlFormatterShortcutApply: "套用設定",
sqlFormatterConfigErrorInvalidJson: "JSON 無效。",
sqlFormatterConfigErrorObject: "設定必須是 JSON 物件。",
sqlFormatterConfigErrorVersion: "不支援的設定版本。",
sqlFormatterConfigErrorFormatter: "不支援的格式化器。",
sqlFormatterConfigErrorUnsupportedParams: "不支援 params 參數替換。SQL 格式化不會替換佔位符。",
sqlFormatterConfigErrorOptionsObject: "設定選項必須是 JSON 物件。",
sqlFormatterConfigErrorEditorObject: "編輯器設定必須是 JSON 物件。",
sqlFormatterConfigErrorEditorScope: "不支援的編輯器作用域。",
sqlFormatterConfigErrorEditorPlatforms: "編輯器平台設定無效。",
sqlFormatterConfigErrorEditorShortcutsArray: "編輯器快速鍵必須是陣列。",
sqlFormatterConfigErrorEditorShortcutValue: "編輯器快速鍵無效。",
sqlFormatterConfigErrorUnknownOption: "未知格式化選項:{option}。",
sqlFormatterConfigErrorInvalidOptionValue: "「{option}」的值無效。",
sqlFormatterConfigErrorUnknownEditorShortcut: "未知編輯器快速鍵:{shortcut}。",
sqlFormatterConfigErrorInvalidEditorShortcut: "編輯器快速鍵無效:{shortcut}。",
sqlFormatterConfigErrorDuplicateEditorShortcut: "編輯器快速鍵重複:{shortcut}。",
sqlFormatterConfigErrorInvalidConfig: "格式化設定無效。",
appearanceTab: "外觀",
navigationTab: "導覽",

View File

@ -19,7 +19,7 @@ function formatterLanguage(dialect: SqlFormatDialect) {
}
}
export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "generic", settings: SqlFormatterSettings = DEFAULT_SQL_FORMATTER_SETTINGS): Promise<string> {
export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "generic", settings: Partial<SqlFormatterSettings> = DEFAULT_SQL_FORMATTER_SETTINGS): Promise<string> {
if (!sql.trim()) return sql;
if (sql.length > MAX_SQL_FORMAT_CHARS) {
throw new Error("SQL is too large to format safely.");

View File

@ -1,22 +1,44 @@
export const SQL_FORMATTER_CONFIG_VERSION = 1;
export const SQL_FORMATTER_CONFIG_FORMATTER = "sql-formatter";
export const SQL_FORMATTER_EDITOR_SCOPE = "sqlFormatterConfigJsonEditor";
const CASE_VALUES = ["preserve", "upper", "lower"] as const;
const INDENT_STYLE_VALUES = ["standard", "tabularLeft", "tabularRight"] as const;
const LOGICAL_OPERATOR_NEWLINE_VALUES = ["before", "after"] as const;
const TAB_WIDTH_VALUES = [2, 4] as const;
const EXPRESSION_WIDTH_VALUES = [50, 80, 120] as const;
const LINES_BETWEEN_QUERIES_VALUES = [0, 1, 2] as const;
const SQL_FORMATTER_PLATFORM_VALUES = ["windows", "linux", "macos"] as const;
const SQL_FORMATTER_PARAM_TYPE_MARKERS = ["?", ":", "$"] as const;
const SQL_FORMATTER_NAMED_PARAM_TYPE_MARKERS = [":", "@", "$"] as const;
const SQL_FORMATTER_LEGACY_OPTION_KEYS = new Set(["params"]);
export type SqlFormatterCase = (typeof CASE_VALUES)[number];
export type SqlFormatterIndentStyle = (typeof INDENT_STYLE_VALUES)[number];
export type SqlFormatterLogicalOperatorNewline = (typeof LOGICAL_OPERATOR_NEWLINE_VALUES)[number];
export type SqlFormatterTabWidth = (typeof TAB_WIDTH_VALUES)[number];
export type SqlFormatterExpressionWidth = (typeof EXPRESSION_WIDTH_VALUES)[number];
export type SqlFormatterLinesBetweenQueries = (typeof LINES_BETWEEN_QUERIES_VALUES)[number];
export type SqlFormatterPlatform = (typeof SQL_FORMATTER_PLATFORM_VALUES)[number];
export interface SqlFormatterSettings {
export interface SqlFormatterCustomParameter {
regex: string;
}
export interface SqlFormatterParamTypes {
positional?: boolean;
numbered?: ("?" | ":" | "$")[];
named?: (":" | "@" | "$")[];
quoted?: (":" | "@" | "$")[];
custom?: SqlFormatterCustomParameter[];
}
export interface SqlFormatterOptionSettings {
keywordCase: SqlFormatterCase;
dataTypeCase: SqlFormatterCase;
functionCase: SqlFormatterCase;
identifierCase: SqlFormatterCase;
indentStyle: SqlFormatterIndentStyle;
useTabs: boolean;
tabWidth: SqlFormatterTabWidth;
logicalOperatorNewline: SqlFormatterLogicalOperatorNewline;
@ -24,20 +46,70 @@ export interface SqlFormatterSettings {
linesBetweenQueries: SqlFormatterLinesBetweenQueries;
denseOperators: boolean;
newlineBeforeSemicolon: boolean;
paramTypes: SqlFormatterParamTypes | null;
}
export type SqlFormatterEditorShortcutId = "find" | "replace" | "indentMore" | "indentLess" | "duplicateLine" | "deleteLine" | "moveLineUp" | "moveLineDown" | "copyLineUp" | "copyLineDown" | "undo" | "redo" | "selectAll" | "formatJson" | "applyConfig";
export type SqlFormatterEditorShortcutAction = "openSearchPanel" | "indentMore" | "indentLess" | "copyLineDown" | "copyLineUp" | "deleteLine" | "moveLineUp" | "moveLineDown" | "undo" | "redo" | "selectAll" | "formatJson" | "applyJsonDraft";
export interface SqlFormatterEditorShortcutKeys {
windows: string;
linux: string;
macos: string;
}
export interface SqlFormatterEditorShortcut {
id: SqlFormatterEditorShortcutId;
action: SqlFormatterEditorShortcutAction;
keys: SqlFormatterEditorShortcutKeys;
enabled: boolean;
}
export interface SqlFormatterEditorSettings {
scope: typeof SQL_FORMATTER_EDITOR_SCOPE;
platforms: SqlFormatterPlatform[];
shortcuts: SqlFormatterEditorShortcut[];
}
export interface SqlFormatterSettings extends SqlFormatterOptionSettings {
editor: SqlFormatterEditorSettings;
}
export interface SqlFormatterConfigFile {
version: typeof SQL_FORMATTER_CONFIG_VERSION;
formatter: typeof SQL_FORMATTER_CONFIG_FORMATTER;
options: SqlFormatterSettings;
options: SqlFormatterOptionSettings;
editor: SqlFormatterEditorSettings;
}
export type SqlFormatterConfigParseResult = { ok: true; settings: SqlFormatterSettings } | { ok: false; message: string };
export type SqlFormatterEditorValidationResult = { ok: true } | { ok: false; message: string };
export const DEFAULT_SQL_FORMATTER_EDITOR_SHORTCUTS: SqlFormatterEditorShortcut[] = [
{ id: "find", action: "openSearchPanel", keys: { windows: "Ctrl+F", linux: "Ctrl+F", macos: "Cmd+F" }, enabled: true },
{ id: "replace", action: "openSearchPanel", keys: { windows: "Ctrl+H", linux: "Ctrl+H", macos: "Cmd+Option+F" }, enabled: true },
{ id: "indentMore", action: "indentMore", keys: { windows: "Tab", linux: "Tab", macos: "Tab" }, enabled: true },
{ id: "indentLess", action: "indentLess", keys: { windows: "Shift+Tab", linux: "Shift+Tab", macos: "Shift+Tab" }, enabled: true },
{ id: "duplicateLine", action: "copyLineDown", keys: { windows: "Ctrl+D", linux: "Ctrl+D", macos: "Cmd+D" }, enabled: true },
{ id: "deleteLine", action: "deleteLine", keys: { windows: "Ctrl+Shift+K", linux: "Ctrl+Shift+K", macos: "Cmd+Shift+K" }, enabled: true },
{ id: "moveLineUp", action: "moveLineUp", keys: { windows: "Alt+Up", linux: "Alt+Up", macos: "Option+Up" }, enabled: true },
{ id: "moveLineDown", action: "moveLineDown", keys: { windows: "Alt+Down", linux: "Alt+Down", macos: "Option+Down" }, enabled: true },
{ id: "copyLineUp", action: "copyLineUp", keys: { windows: "Shift+Alt+Up", linux: "Shift+Alt+Up", macos: "Shift+Option+Up" }, enabled: true },
{ id: "copyLineDown", action: "copyLineDown", keys: { windows: "Shift+Alt+Down", linux: "Shift+Alt+Down", macos: "Shift+Option+Down" }, enabled: true },
{ id: "undo", action: "undo", keys: { windows: "Ctrl+Z", linux: "Ctrl+Z", macos: "Cmd+Z" }, enabled: true },
{ id: "redo", action: "redo", keys: { windows: "Ctrl+Y", linux: "Ctrl+Shift+Z", macos: "Cmd+Shift+Z" }, enabled: true },
{ id: "selectAll", action: "selectAll", keys: { windows: "Ctrl+A", linux: "Ctrl+A", macos: "Cmd+A" }, enabled: true },
{ id: "formatJson", action: "formatJson", keys: { windows: "Shift+Alt+F", linux: "Shift+Alt+F", macos: "Shift+Cmd+F" }, enabled: true },
{ id: "applyConfig", action: "applyJsonDraft", keys: { windows: "Ctrl+S", linux: "Ctrl+S", macos: "Cmd+S" }, enabled: true },
];
export const DEFAULT_SQL_FORMATTER_SETTINGS: SqlFormatterSettings = {
keywordCase: "upper",
dataTypeCase: "preserve",
functionCase: "preserve",
identifierCase: "preserve",
indentStyle: "standard",
useTabs: false,
tabWidth: 2,
logicalOperatorNewline: "before",
@ -45,14 +117,36 @@ export const DEFAULT_SQL_FORMATTER_SETTINGS: SqlFormatterSettings = {
linesBetweenQueries: 1,
denseOperators: false,
newlineBeforeSemicolon: false,
paramTypes: null,
editor: {
scope: SQL_FORMATTER_EDITOR_SCOPE,
platforms: [...SQL_FORMATTER_PLATFORM_VALUES],
shortcuts: cloneShortcuts(DEFAULT_SQL_FORMATTER_EDITOR_SHORTCUTS),
},
};
const SQL_FORMATTER_OPTION_KEYS = new Set<keyof SqlFormatterSettings>(["keywordCase", "dataTypeCase", "functionCase", "useTabs", "tabWidth", "logicalOperatorNewline", "expressionWidth", "linesBetweenQueries", "denseOperators", "newlineBeforeSemicolon"]);
const SQL_FORMATTER_OPTION_KEYS = new Set<keyof SqlFormatterOptionSettings>([
"keywordCase",
"dataTypeCase",
"functionCase",
"identifierCase",
"indentStyle",
"useTabs",
"tabWidth",
"logicalOperatorNewline",
"expressionWidth",
"linesBetweenQueries",
"denseOperators",
"newlineBeforeSemicolon",
"paramTypes",
]);
const SQL_FORMATTER_OPTION_VALIDATORS: Record<keyof SqlFormatterSettings, (value: unknown) => boolean> = {
const SQL_FORMATTER_OPTION_VALIDATORS: Record<keyof SqlFormatterOptionSettings, (value: unknown) => boolean> = {
keywordCase: (value) => isStringChoice(value, CASE_VALUES),
dataTypeCase: (value) => isStringChoice(value, CASE_VALUES),
functionCase: (value) => isStringChoice(value, CASE_VALUES),
identifierCase: (value) => isStringChoice(value, CASE_VALUES),
indentStyle: (value) => isStringChoice(value, INDENT_STYLE_VALUES),
useTabs: (value) => typeof value === "boolean",
tabWidth: (value) => isNumberChoice(value, TAB_WIDTH_VALUES),
logicalOperatorNewline: (value) => isStringChoice(value, LOGICAL_OPERATOR_NEWLINE_VALUES),
@ -60,6 +154,7 @@ const SQL_FORMATTER_OPTION_VALIDATORS: Record<keyof SqlFormatterSettings, (value
linesBetweenQueries: (value) => isNumberChoice(value, LINES_BETWEEN_QUERIES_VALUES),
denseOperators: (value) => typeof value === "boolean",
newlineBeforeSemicolon: (value) => typeof value === "boolean",
paramTypes: isSqlFormatterParamTypes,
};
function isObject(value: unknown): value is Record<string, unknown> {
@ -74,6 +169,32 @@ function isNumberChoice(value: unknown, values: readonly number[]): boolean {
return typeof value === "number" && values.includes(value);
}
function isMarkerArray<T extends readonly string[]>(value: unknown, markers: T): value is T[number][] {
return Array.isArray(value) && value.every((item) => typeof item === "string" && markers.includes(item));
}
function isCustomParameter(value: unknown): value is SqlFormatterCustomParameter {
if (!isObject(value) || typeof value.regex !== "string" || value.regex.length === 0 || !Object.keys(value).every((key) => key === "regex")) return false;
try {
new RegExp(`(?:${value.regex})`, "uy");
return true;
} catch {
return false;
}
}
function isSqlFormatterParamTypes(value: unknown): value is SqlFormatterParamTypes | null {
if (value === null) return true;
if (!isObject(value)) return false;
if (!Object.keys(value).every((key) => ["positional", "numbered", "named", "quoted", "custom"].includes(key))) return false;
if (value.positional !== undefined && typeof value.positional !== "boolean") return false;
if (value.numbered !== undefined && !isMarkerArray(value.numbered, SQL_FORMATTER_PARAM_TYPE_MARKERS)) return false;
if (value.named !== undefined && !isMarkerArray(value.named, SQL_FORMATTER_NAMED_PARAM_TYPE_MARKERS)) return false;
if (value.quoted !== undefined && !isMarkerArray(value.quoted, SQL_FORMATTER_NAMED_PARAM_TYPE_MARKERS)) return false;
if (value.custom !== undefined && (!Array.isArray(value.custom) || !value.custom.every(isCustomParameter))) return false;
return true;
}
function normalizeChoice<T extends readonly string[]>(value: unknown, values: T, fallback: T[number]): T[number] {
return typeof value === "string" && values.includes(value) ? value : fallback;
}
@ -86,12 +207,181 @@ function normalizeBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
export function normalizeSqlFormatterSettings(value: unknown): SqlFormatterSettings {
function normalizeParamTypes(value: unknown, fallback: SqlFormatterParamTypes | null): SqlFormatterParamTypes | null {
if (value === null) return null;
if (!isSqlFormatterParamTypes(value)) return fallback;
return {
...(value.positional !== undefined ? { positional: value.positional } : {}),
...(value.numbered ? { numbered: [...value.numbered] } : {}),
...(value.named ? { named: [...value.named] } : {}),
...(value.quoted ? { quoted: [...value.quoted] } : {}),
...(value.custom ? { custom: value.custom.map((item) => ({ regex: item.regex })) } : {}),
};
}
function cloneShortcuts(shortcuts: readonly SqlFormatterEditorShortcut[]): SqlFormatterEditorShortcut[] {
return shortcuts.map((shortcut) => ({
id: shortcut.id,
action: shortcut.action,
keys: { ...shortcut.keys },
enabled: shortcut.enabled,
}));
}
function normalizeShortcutKeys(value: unknown, fallback: SqlFormatterEditorShortcutKeys): SqlFormatterEditorShortcutKeys {
const input = isObject(value) ? value : {};
return {
windows: typeof input.windows === "string" ? input.windows.trim() : fallback.windows,
linux: typeof input.linux === "string" ? input.linux.trim() : fallback.linux,
macos: typeof input.macos === "string" ? input.macos.trim() : fallback.macos,
};
}
function isCompatibleShortcutAction(shortcut: Record<string, unknown>, fallback: SqlFormatterEditorShortcut): boolean {
if (shortcut.action === undefined || shortcut.action === fallback.action || shortcut.action === fallback.id) return true;
return fallback.id === "replace" && shortcut.action === "openReplacePanel";
}
export function sqlFormatterPlatformFromNavigator(platform = globalThis.navigator?.platform || ""): SqlFormatterPlatform {
const normalized = platform.toLowerCase();
if (normalized.includes("mac")) return "macos";
if (normalized.includes("linux")) return "linux";
return "windows";
}
function keyPartToCodeMirror(part: string): string | null {
const normalized = part.trim().toLowerCase();
if (normalized === "ctrl" || normalized === "control") return "Ctrl";
if (normalized === "cmd" || normalized === "command") return "Mod";
if (normalized === "option" || normalized === "alt") return "Alt";
if (normalized === "shift") return "Shift";
if (normalized === "up") return "ArrowUp";
if (normalized === "down") return "ArrowDown";
if (normalized === "left") return "ArrowLeft";
if (normalized === "right") return "ArrowRight";
if (normalized === "esc") return "Escape";
if (normalized === "enter" || normalized === "return") return "Enter";
if (normalized === "del") return "Delete";
if (normalized === "space") return "Space";
if (normalized === "pageup") return "PageUp";
if (normalized === "pagedown") return "PageDown";
if (["tab", "escape", "backspace", "delete", "home", "end"].includes(normalized)) return normalized[0].toUpperCase() + normalized.slice(1);
if (/^f(?:[1-9]|1\d|2[0-4])$/.test(normalized)) return normalized.toUpperCase();
if (/^[a-z0-9]$/.test(normalized)) return normalized;
return null;
}
export function sqlFormatterShortcutDisplayKeyToCodeMirrorKey(value: string): string | null {
const parts = value
.split("+")
.map((part) => part.trim())
.filter(Boolean);
if (!parts.length) return null;
const converted = parts.map(keyPartToCodeMirror);
if (converted.some((part) => !part)) return null;
const key = converted[converted.length - 1];
if (!key || ["Ctrl", "Mod", "Alt", "Shift"].includes(key)) return null;
const modifiers = converted.slice(0, -1);
if (new Set(modifiers).size !== modifiers.length) return null;
if (modifiers.some((part) => part && !["Ctrl", "Mod", "Alt", "Shift"].includes(part))) return null;
return [...modifiers, key].join("-");
}
function shortcutConflictKey(value: string): string | null {
return sqlFormatterShortcutDisplayKeyToCodeMirrorKey(value)?.toLowerCase() ?? null;
}
export function validateSqlFormatterEditorSettings(value: unknown): SqlFormatterEditorValidationResult {
const settings = normalizeSqlFormatterEditorSettings(value);
const platformSet = new Set(settings.platforms);
for (const shortcut of settings.shortcuts) {
if (!shortcut.enabled) continue;
for (const platform of settings.platforms) {
if (!shortcutConflictKey(shortcut.keys[platform])) return { ok: false, message: `Invalid editor shortcut value: ${shortcut.id}.` };
}
}
for (const platform of SQL_FORMATTER_PLATFORM_VALUES) {
if (!platformSet.has(platform)) continue;
const seen = new Map<string, SqlFormatterEditorShortcutId>();
for (const shortcut of settings.shortcuts) {
if (!shortcut.enabled) continue;
const key = shortcutConflictKey(shortcut.keys[platform]);
if (!key) continue;
const existing = seen.get(key);
if (existing) return { ok: false, message: `Duplicate editor shortcut: ${platform}:${shortcut.keys[platform]}.` };
seen.set(key, shortcut.id);
}
}
return { ok: true };
}
export function normalizeSqlFormatterEditorSettings(value: unknown): SqlFormatterEditorSettings {
const input = isObject(value) ? value : {};
const shortcutsInput = Array.isArray(input.shortcuts) ? input.shortcuts.filter(isObject) : [];
const platforms = Array.isArray(input.platforms) ? input.platforms.filter((platform): platform is SqlFormatterPlatform => isStringChoice(platform, SQL_FORMATTER_PLATFORM_VALUES)) : [...SQL_FORMATTER_PLATFORM_VALUES];
const normalizedPlatforms = platforms.length ? [...new Set(platforms)] : [...SQL_FORMATTER_PLATFORM_VALUES];
return {
scope: SQL_FORMATTER_EDITOR_SCOPE,
platforms: normalizedPlatforms,
shortcuts: DEFAULT_SQL_FORMATTER_EDITOR_SHORTCUTS.map((fallback) => {
const source = shortcutsInput.find((shortcut) => shortcut.id === fallback.id);
return {
id: fallback.id,
action: fallback.action,
keys: normalizeShortcutKeys(source?.keys, fallback.keys),
enabled: typeof source?.enabled === "boolean" ? source.enabled : fallback.enabled,
};
}),
};
}
function validateEditorConfig(value: unknown): string | null {
if (value === undefined) return null;
if (!isObject(value)) return "Config editor must be a JSON object.";
if (value.scope !== undefined && value.scope !== SQL_FORMATTER_EDITOR_SCOPE) return "Unsupported editor scope.";
if (value.platforms !== undefined && (!Array.isArray(value.platforms) || !value.platforms.every((platform) => isStringChoice(platform, SQL_FORMATTER_PLATFORM_VALUES)))) {
return "Invalid editor platforms.";
}
if (value.shortcuts === undefined) return null;
if (!Array.isArray(value.shortcuts)) return "Config editor shortcuts must be an array.";
for (const shortcut of value.shortcuts) {
if (!isObject(shortcut) || typeof shortcut.id !== "string") return "Invalid editor shortcut value.";
const fallback = DEFAULT_SQL_FORMATTER_EDITOR_SHORTCUTS.find((item) => item.id === shortcut.id);
if (!fallback) return `Unknown editor shortcut: ${shortcut.id}.`;
if (!isCompatibleShortcutAction(shortcut, fallback)) return `Invalid editor shortcut value: ${shortcut.id}.`;
if (shortcut.enabled !== undefined && typeof shortcut.enabled !== "boolean") return `Invalid editor shortcut value: ${shortcut.id}.`;
if (shortcut.keys !== undefined) {
if (!isObject(shortcut.keys)) return `Invalid editor shortcut value: ${shortcut.id}.`;
const keys = Object.entries(shortcut.keys);
const hasInvalidKey = keys.some(([platform, key]) => !isStringChoice(platform, SQL_FORMATTER_PLATFORM_VALUES) || typeof key !== "string" || !key.trim());
if (hasInvalidKey) {
return `Invalid editor shortcut value: ${shortcut.id}.`;
}
}
}
const normalized = normalizeSqlFormatterEditorSettings(value);
const validation = validateSqlFormatterEditorSettings(normalized);
return validation.ok ? null : validation.message;
}
export function sqlFormatterOptionSettings(settings: unknown): SqlFormatterOptionSettings {
const input = isObject(settings) ? settings : {};
return {
keywordCase: normalizeChoice(input.keywordCase, CASE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase),
dataTypeCase: normalizeChoice(input.dataTypeCase, CASE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.dataTypeCase),
functionCase: normalizeChoice(input.functionCase, CASE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.functionCase),
identifierCase: normalizeChoice(input.identifierCase, CASE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.identifierCase),
indentStyle: normalizeChoice(input.indentStyle, INDENT_STYLE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.indentStyle),
useTabs: normalizeBoolean(input.useTabs, DEFAULT_SQL_FORMATTER_SETTINGS.useTabs),
tabWidth: normalizeNumberChoice(input.tabWidth, TAB_WIDTH_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.tabWidth),
logicalOperatorNewline: normalizeChoice(input.logicalOperatorNewline, LOGICAL_OPERATOR_NEWLINE_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.logicalOperatorNewline),
@ -99,14 +389,26 @@ export function normalizeSqlFormatterSettings(value: unknown): SqlFormatterSetti
linesBetweenQueries: normalizeNumberChoice(input.linesBetweenQueries, LINES_BETWEEN_QUERIES_VALUES, DEFAULT_SQL_FORMATTER_SETTINGS.linesBetweenQueries),
denseOperators: normalizeBoolean(input.denseOperators, DEFAULT_SQL_FORMATTER_SETTINGS.denseOperators),
newlineBeforeSemicolon: normalizeBoolean(input.newlineBeforeSemicolon, DEFAULT_SQL_FORMATTER_SETTINGS.newlineBeforeSemicolon),
paramTypes: normalizeParamTypes(input.paramTypes, DEFAULT_SQL_FORMATTER_SETTINGS.paramTypes),
};
}
export function normalizeSqlFormatterSettings(value: unknown): SqlFormatterSettings {
const input = isObject(value) ? value : {};
const optionSource = isObject(input.options) ? input.options : input;
return {
...sqlFormatterOptionSettings(optionSource),
editor: normalizeSqlFormatterEditorSettings(input.editor),
};
}
export function sqlFormatterConfigFile(settings: unknown): SqlFormatterConfigFile {
const normalized = normalizeSqlFormatterSettings(settings);
return {
version: SQL_FORMATTER_CONFIG_VERSION,
formatter: SQL_FORMATTER_CONFIG_FORMATTER,
options: normalizeSqlFormatterSettings(settings),
options: sqlFormatterOptionSettings(normalized),
editor: normalizeSqlFormatterEditorSettings(normalized.editor),
};
}
@ -127,16 +429,21 @@ export function parseSqlFormatterConfig(text: string): SqlFormatterConfigParseRe
if (parsed.formatter !== SQL_FORMATTER_CONFIG_FORMATTER) return { ok: false, message: "Unsupported formatter." };
if (!isObject(parsed.options)) return { ok: false, message: "Config options must be a JSON object." };
const unknownOption = Object.keys(parsed.options).find((key) => !SQL_FORMATTER_OPTION_KEYS.has(key as keyof SqlFormatterSettings));
const unknownOption = Object.keys(parsed.options).find((key) => !SQL_FORMATTER_OPTION_KEYS.has(key as keyof SqlFormatterOptionSettings) && !SQL_FORMATTER_LEGACY_OPTION_KEYS.has(key));
if (unknownOption) return { ok: false, message: `Unknown formatter option: ${unknownOption}.` };
if ("params" in parsed.options && parsed.options.params !== null) return { ok: false, message: "Unsupported formatter option: params." };
const invalidOption = Object.entries(parsed.options).find(([key, value]) => {
const optionKey = key as keyof SqlFormatterSettings;
if (SQL_FORMATTER_LEGACY_OPTION_KEYS.has(key)) return false;
const optionKey = key as keyof SqlFormatterOptionSettings;
return !SQL_FORMATTER_OPTION_VALIDATORS[optionKey](value);
});
if (invalidOption) return { ok: false, message: `Invalid formatter option value: ${invalidOption[0]}.` };
return { ok: true, settings: normalizeSqlFormatterSettings(parsed.options) };
const editorError = validateEditorConfig(parsed.editor);
if (editorError) return { ok: false, message: editorError };
return { ok: true, settings: normalizeSqlFormatterSettings({ ...parsed.options, editor: parsed.editor }) };
}
export function syncSqlFormatterConfigDraft(text: string, syncSettings: (settings: SqlFormatterSettings) => void): SqlFormatterConfigParseResult {
@ -146,11 +453,13 @@ export function syncSqlFormatterConfigDraft(text: string, syncSettings: (setting
}
export function sqlFormatterOptions(settings: unknown) {
const normalized = normalizeSqlFormatterSettings(settings);
const normalized = sqlFormatterOptionSettings(settings);
return {
keywordCase: normalized.keywordCase,
dataTypeCase: normalized.dataTypeCase,
functionCase: normalized.functionCase,
identifierCase: normalized.identifierCase,
indentStyle: normalized.indentStyle,
useTabs: normalized.useTabs,
tabWidth: normalized.tabWidth,
logicalOperatorNewline: normalized.logicalOperatorNewline,
@ -158,5 +467,6 @@ export function sqlFormatterOptions(settings: unknown) {
linesBetweenQueries: normalized.linesBetweenQueries,
denseOperators: normalized.denseOperators,
newlineBeforeSemicolon: normalized.newlineBeforeSemicolon,
...(normalized.paramTypes !== null ? { paramTypes: normalized.paramTypes } : {}),
};
}

View File

@ -1,4 +1,5 @@
import type { Command, KeyBinding } from "@codemirror/view";
import { normalizeSqlFormatterEditorSettings, sqlFormatterPlatformFromNavigator, sqlFormatterShortcutDisplayKeyToCodeMirrorKey, type SqlFormatterEditorSettings, type SqlFormatterEditorShortcutAction, type SqlFormatterEditorShortcutId } from "@/lib/sqlFormatterConfig";
export interface SqlFormatterConfigEditorCommands {
indentMore: Command;
@ -8,6 +9,9 @@ export interface SqlFormatterConfigEditorCommands {
deleteLine: Command;
moveLineUp: Command;
moveLineDown: Command;
undo: Command;
redo: Command;
selectAll: Command;
openSearchPanel: Command;
}
@ -16,20 +20,75 @@ export interface SqlFormatterConfigEditorActions {
formatJson: Command;
}
export function createSqlFormatterConfigKeymap(commands: SqlFormatterConfigEditorCommands, actions: SqlFormatterConfigEditorActions): KeyBinding[] {
return [
{ key: "Tab", run: commands.indentMore },
{ key: "Shift-Tab", run: commands.indentLess },
{ key: "Mod-d", run: commands.copyLineDown },
{ key: "Shift-Mod-k", run: commands.deleteLine },
{ key: "Alt-ArrowUp", run: commands.moveLineUp },
{ key: "Alt-ArrowDown", run: commands.moveLineDown },
{ key: "Shift-Alt-ArrowUp", run: commands.copyLineUp },
{ key: "Shift-Alt-ArrowDown", run: commands.copyLineDown },
{ key: "Ctrl-h", mac: "Mod-Alt-f", run: commands.openSearchPanel, preventDefault: true },
{ key: "Shift-Alt-f", mac: "Shift-Mod-f", run: actions.formatJson, preventDefault: true },
{ key: "Mod-s", run: actions.apply, preventDefault: true },
];
const shortcutLabelKeys: Record<SqlFormatterEditorShortcutId, string> = {
find: "settings.sqlFormatterShortcutFind",
replace: "settings.sqlFormatterShortcutReplace",
indentMore: "settings.sqlFormatterShortcutIndentMore",
indentLess: "settings.sqlFormatterShortcutIndentLess",
duplicateLine: "settings.sqlFormatterShortcutDuplicateLine",
deleteLine: "settings.sqlFormatterShortcutDeleteLine",
moveLineUp: "settings.sqlFormatterShortcutMoveLineUp",
moveLineDown: "settings.sqlFormatterShortcutMoveLineDown",
copyLineUp: "settings.sqlFormatterShortcutCopyLineUp",
copyLineDown: "settings.sqlFormatterShortcutCopyLineDown",
undo: "settings.sqlFormatterShortcutUndo",
redo: "settings.sqlFormatterShortcutRedo",
selectAll: "settings.sqlFormatterShortcutSelectAll",
formatJson: "settings.sqlFormatterShortcutFormatJson",
applyConfig: "settings.sqlFormatterShortcutApply",
};
export function sqlFormatterConfigShortcutLabelKey(id: SqlFormatterEditorShortcutId): string {
return shortcutLabelKeys[id];
}
function commandForAction(commands: SqlFormatterConfigEditorCommands, actions: SqlFormatterConfigEditorActions, action: SqlFormatterEditorShortcutAction): Command {
switch (action) {
case "indentMore":
return commands.indentMore;
case "indentLess":
return commands.indentLess;
case "copyLineDown":
return commands.copyLineDown;
case "copyLineUp":
return commands.copyLineUp;
case "deleteLine":
return commands.deleteLine;
case "moveLineUp":
return commands.moveLineUp;
case "moveLineDown":
return commands.moveLineDown;
case "undo":
return commands.undo;
case "redo":
return commands.redo;
case "selectAll":
return commands.selectAll;
case "openSearchPanel":
return commands.openSearchPanel;
case "formatJson":
return actions.formatJson;
case "applyJsonDraft":
return actions.apply;
}
}
export function createSqlFormatterConfigKeymap(commands: SqlFormatterConfigEditorCommands, actions: SqlFormatterConfigEditorActions, editorSettings?: SqlFormatterEditorSettings): KeyBinding[] {
const settings = normalizeSqlFormatterEditorSettings(editorSettings);
const platforms = new Set(settings.platforms);
return settings.shortcuts
.filter((shortcut) => shortcut.enabled)
.map((shortcut) => {
const binding: KeyBinding = {
run: commandForAction(commands, actions, shortcut.action),
preventDefault: true,
};
if (platforms.has("windows")) binding.win = sqlFormatterShortcutDisplayKeyToCodeMirrorKey(shortcut.keys.windows) ?? undefined;
if (platforms.has("linux")) binding.linux = sqlFormatterShortcutDisplayKeyToCodeMirrorKey(shortcut.keys.linux) ?? undefined;
if (platforms.has("macos")) binding.mac = sqlFormatterShortcutDisplayKeyToCodeMirrorKey(shortcut.keys.macos) ?? undefined;
return binding;
});
}
export interface SqlFormatterConfigShortcutRow {
@ -38,37 +97,14 @@ export interface SqlFormatterConfigShortcutRow {
shortcut: string;
}
function modLabel(platform = globalThis.navigator?.platform || ""): "Cmd" | "Ctrl" {
return platform.toLowerCase().includes("mac") ? "Cmd" : "Ctrl";
}
export function sqlFormatterConfigShortcutRows(platform = globalThis.navigator?.platform || "", editorSettings?: SqlFormatterEditorSettings): SqlFormatterConfigShortcutRow[] {
const platformKey = sqlFormatterPlatformFromNavigator(platform);
function altLabel(platform = globalThis.navigator?.platform || ""): "Option" | "Alt" {
return platform.toLowerCase().includes("mac") ? "Option" : "Alt";
}
export function sqlFormatterConfigShortcutRows(platform = globalThis.navigator?.platform || ""): SqlFormatterConfigShortcutRow[] {
const isMac = platform.toLowerCase().includes("mac");
const mod = modLabel(platform);
const alt = altLabel(platform);
return [
{ id: "find", labelKey: "settings.sqlFormatterShortcutFind", shortcut: `${mod}+F` },
{
id: "replace",
labelKey: "settings.sqlFormatterShortcutReplace",
shortcut: isMac ? "Cmd+Option+F" : "Ctrl+H",
},
{ id: "indentMore", labelKey: "settings.sqlFormatterShortcutIndentMore", shortcut: "Tab" },
{ id: "indentLess", labelKey: "settings.sqlFormatterShortcutIndentLess", shortcut: "Shift+Tab" },
{ id: "duplicateLine", labelKey: "settings.sqlFormatterShortcutDuplicateLine", shortcut: `${mod}+D` },
{ id: "deleteLine", labelKey: "settings.sqlFormatterShortcutDeleteLine", shortcut: `${mod}+Shift+K` },
{ id: "moveLine", labelKey: "settings.sqlFormatterShortcutMoveLine", shortcut: `${alt}+Up/Down` },
{ id: "copyLine", labelKey: "settings.sqlFormatterShortcutCopyLine", shortcut: `Shift+${alt}+Up/Down` },
{
id: "formatJson",
labelKey: "settings.sqlFormatterShortcutFormatJson",
shortcut: isMac ? "Shift+Cmd+F" : "Shift+Alt+F",
},
{ id: "apply", labelKey: "settings.sqlFormatterShortcutApply", shortcut: `${mod}+S` },
];
return normalizeSqlFormatterEditorSettings(editorSettings)
.shortcuts.filter((shortcut) => shortcut.enabled)
.map((shortcut) => ({
id: shortcut.id,
labelKey: shortcutLabelKeys[shortcut.id],
shortcut: shortcut.keys[platformKey],
}));
}

View File

@ -313,7 +313,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
cellDetailDrawerWidth: 320,
cellDetailPanelLayout: "bottom",
shortcuts: normalizeShortcutSettings(),
sqlFormatter: { ...DEFAULT_SQL_FORMATTER_SETTINGS },
sqlFormatter: normalizeSqlFormatterSettings(DEFAULT_SQL_FORMATTER_SETTINGS),
sidebarActivation: "single",
sidebarObjectDisplay: "grouped",
autoSelectActiveSidebarNode: false,

View File

@ -29,6 +29,7 @@
"---设置---",
"plugins",
"driver-management",
"sql-formatter",
"keyboard-shortcuts",
"connection-import",
"config-export",

View File

@ -29,6 +29,7 @@
"---Settings---",
"plugins",
"driver-management",
"sql-formatter",
"keyboard-shortcuts",
"connection-import",
"config-export",

View File

@ -0,0 +1,225 @@
---
title: SQL 格式化
description: 配置 SQL 格式化选项、导入导出 JSON 配置,并定制 JSON 配置编辑器快捷键。
---
DBX 的 SQL 格式化配置位于**设置 → SQL 格式化**。配置分为两部分:
- `options`:控制 SQL 格式化结果,例如关键字大小写、缩进、操作符换行和参数类型。
- `editor`:控制 SQL 格式化 JSON 配置编辑器中的快捷键。
## 表单与 JSON
表单模式适合修改常用选项。JSON 模式适合复制、粘贴、导入或批量修改完整配置。
JSON 配置合法时DBX 会把配置同步回表单;配置无效时,会显示具体错误并阻止应用。
## 快捷键平台
快捷键配置文件可以同时保存 Windows、Linux 和 macOS 三个平台的按键。
表单中只显示当前系统的平台列。例如在 macOS 上只显示 macOS 快捷键,避免三端配置同时挤在一个表格里。导入和导出仍会保留完整三端配置,便于在不同系统间迁移。
## 完整模板 JSON
```json
{
"version": 1,
"formatter": "sql-formatter",
"name": "DBX SQL 格式化配置",
"description": "SQL 格式化选项,以及 SQL 格式化 JSON 配置编辑器快捷键模板。",
"options": {
"keywordCase": "upper",
"dataTypeCase": "preserve",
"functionCase": "preserve",
"identifierCase": "preserve",
"indentStyle": "standard",
"useTabs": false,
"tabWidth": 2,
"logicalOperatorNewline": "before",
"expressionWidth": 80,
"linesBetweenQueries": 1,
"denseOperators": false,
"newlineBeforeSemicolon": false,
"paramTypes": null
},
"editor": {
"scope": "sqlFormatterConfigJsonEditor",
"platforms": ["windows", "linux", "macos"],
"shortcuts": [
{
"id": "find",
"action": "openSearchPanel",
"label": "查找",
"keys": {
"windows": "Ctrl+F",
"linux": "Ctrl+F",
"macos": "Cmd+F"
},
"enabled": true
},
{
"id": "replace",
"action": "openSearchPanel",
"label": "替换",
"keys": {
"windows": "Ctrl+H",
"linux": "Ctrl+H",
"macos": "Cmd+Option+F"
},
"enabled": true
},
{
"id": "indentMore",
"action": "indentMore",
"label": "增加缩进",
"keys": {
"windows": "Tab",
"linux": "Tab",
"macos": "Tab"
},
"enabled": true
},
{
"id": "indentLess",
"action": "indentLess",
"label": "减少缩进",
"keys": {
"windows": "Shift+Tab",
"linux": "Shift+Tab",
"macos": "Shift+Tab"
},
"enabled": true
},
{
"id": "duplicateLine",
"action": "copyLineDown",
"label": "复制当前行",
"keys": {
"windows": "Ctrl+D",
"linux": "Ctrl+D",
"macos": "Cmd+D"
},
"enabled": true
},
{
"id": "deleteLine",
"action": "deleteLine",
"label": "删除当前行",
"keys": {
"windows": "Ctrl+Shift+K",
"linux": "Ctrl+Shift+K",
"macos": "Cmd+Shift+K"
},
"enabled": true
},
{
"id": "moveLineUp",
"action": "moveLineUp",
"label": "当前行上移",
"keys": {
"windows": "Alt+Up",
"linux": "Alt+Up",
"macos": "Option+Up"
},
"enabled": true
},
{
"id": "moveLineDown",
"action": "moveLineDown",
"label": "当前行下移",
"keys": {
"windows": "Alt+Down",
"linux": "Alt+Down",
"macos": "Option+Down"
},
"enabled": true
},
{
"id": "copyLineUp",
"action": "copyLineUp",
"label": "向上复制当前行",
"keys": {
"windows": "Shift+Alt+Up",
"linux": "Shift+Alt+Up",
"macos": "Shift+Option+Up"
},
"enabled": true
},
{
"id": "copyLineDown",
"action": "copyLineDown",
"label": "向下复制当前行",
"keys": {
"windows": "Shift+Alt+Down",
"linux": "Shift+Alt+Down",
"macos": "Shift+Option+Down"
},
"enabled": true
},
{
"id": "undo",
"action": "undo",
"label": "撤销",
"keys": {
"windows": "Ctrl+Z",
"linux": "Ctrl+Z",
"macos": "Cmd+Z"
},
"enabled": true
},
{
"id": "redo",
"action": "redo",
"label": "重做",
"keys": {
"windows": "Ctrl+Y",
"linux": "Ctrl+Shift+Z",
"macos": "Cmd+Shift+Z"
},
"enabled": true
},
{
"id": "selectAll",
"action": "selectAll",
"label": "全选",
"keys": {
"windows": "Ctrl+A",
"linux": "Ctrl+A",
"macos": "Cmd+A"
},
"enabled": true
},
{
"id": "formatJson",
"action": "formatJson",
"label": "格式化 JSON 配置",
"keys": {
"windows": "Shift+Alt+F",
"linux": "Shift+Alt+F",
"macos": "Shift+Cmd+F"
},
"enabled": true
},
{
"id": "applyConfig",
"action": "applyJsonDraft",
"label": "应用 JSON 配置",
"keys": {
"windows": "Ctrl+S",
"linux": "Ctrl+S",
"macos": "Cmd+S"
},
"enabled": true
}
]
}
}
```
## 兼容说明
- `version` 当前固定为 `1`。
- `formatter` 当前固定为 `sql-formatter`。
- `params` 参数替换不会被 SQL 格式化执行;需要参数识别时使用 `paramTypes`。
- 旧模板中的 `replace` 动作如果写成 `openReplacePanel`,导入时会自动兼容并归一化。

View File

@ -0,0 +1,225 @@
---
title: SQL Formatter
description: Configure SQL formatting options, import and export JSON config, and customize JSON config editor shortcuts.
---
DBX SQL formatter settings live in **Settings → SQL Formatter**.
- `options` controls formatting output such as keyword case, indentation, line breaks, and parameter types.
- `editor` controls shortcuts used inside the SQL formatter JSON config editor.
## Form And JSON
Use the form for common options. Use JSON mode to copy, paste, import, export, or edit the full config.
When the JSON config is valid, DBX syncs it back to the form. Invalid JSON shows an error and cannot be applied.
## Shortcut Platform
The config file can store Windows, Linux, and macOS shortcuts at the same time.
The form only shows the current system column. For example, macOS shows only macOS shortcuts. Import and export still preserve all platform keys.
## Full Template JSON
```json
{
"version": 1,
"formatter": "sql-formatter",
"name": "DBX SQL Formatter Config",
"description": "SQL formatter options and SQL formatter JSON config editor shortcut template.",
"options": {
"keywordCase": "upper",
"dataTypeCase": "preserve",
"functionCase": "preserve",
"identifierCase": "preserve",
"indentStyle": "standard",
"useTabs": false,
"tabWidth": 2,
"logicalOperatorNewline": "before",
"expressionWidth": 80,
"linesBetweenQueries": 1,
"denseOperators": false,
"newlineBeforeSemicolon": false,
"paramTypes": null
},
"editor": {
"scope": "sqlFormatterConfigJsonEditor",
"platforms": ["windows", "linux", "macos"],
"shortcuts": [
{
"id": "find",
"action": "openSearchPanel",
"label": "Find",
"keys": {
"windows": "Ctrl+F",
"linux": "Ctrl+F",
"macos": "Cmd+F"
},
"enabled": true
},
{
"id": "replace",
"action": "openSearchPanel",
"label": "Replace",
"keys": {
"windows": "Ctrl+H",
"linux": "Ctrl+H",
"macos": "Cmd+Option+F"
},
"enabled": true
},
{
"id": "indentMore",
"action": "indentMore",
"label": "Indent more",
"keys": {
"windows": "Tab",
"linux": "Tab",
"macos": "Tab"
},
"enabled": true
},
{
"id": "indentLess",
"action": "indentLess",
"label": "Indent less",
"keys": {
"windows": "Shift+Tab",
"linux": "Shift+Tab",
"macos": "Shift+Tab"
},
"enabled": true
},
{
"id": "duplicateLine",
"action": "copyLineDown",
"label": "Duplicate current line",
"keys": {
"windows": "Ctrl+D",
"linux": "Ctrl+D",
"macos": "Cmd+D"
},
"enabled": true
},
{
"id": "deleteLine",
"action": "deleteLine",
"label": "Delete current line",
"keys": {
"windows": "Ctrl+Shift+K",
"linux": "Ctrl+Shift+K",
"macos": "Cmd+Shift+K"
},
"enabled": true
},
{
"id": "moveLineUp",
"action": "moveLineUp",
"label": "Move line up",
"keys": {
"windows": "Alt+Up",
"linux": "Alt+Up",
"macos": "Option+Up"
},
"enabled": true
},
{
"id": "moveLineDown",
"action": "moveLineDown",
"label": "Move line down",
"keys": {
"windows": "Alt+Down",
"linux": "Alt+Down",
"macos": "Option+Down"
},
"enabled": true
},
{
"id": "copyLineUp",
"action": "copyLineUp",
"label": "Copy line up",
"keys": {
"windows": "Shift+Alt+Up",
"linux": "Shift+Alt+Up",
"macos": "Shift+Option+Up"
},
"enabled": true
},
{
"id": "copyLineDown",
"action": "copyLineDown",
"label": "Copy line down",
"keys": {
"windows": "Shift+Alt+Down",
"linux": "Shift+Alt+Down",
"macos": "Shift+Option+Down"
},
"enabled": true
},
{
"id": "undo",
"action": "undo",
"label": "Undo",
"keys": {
"windows": "Ctrl+Z",
"linux": "Ctrl+Z",
"macos": "Cmd+Z"
},
"enabled": true
},
{
"id": "redo",
"action": "redo",
"label": "Redo",
"keys": {
"windows": "Ctrl+Y",
"linux": "Ctrl+Shift+Z",
"macos": "Cmd+Shift+Z"
},
"enabled": true
},
{
"id": "selectAll",
"action": "selectAll",
"label": "Select all",
"keys": {
"windows": "Ctrl+A",
"linux": "Ctrl+A",
"macos": "Cmd+A"
},
"enabled": true
},
{
"id": "formatJson",
"action": "formatJson",
"label": "Format JSON config",
"keys": {
"windows": "Shift+Alt+F",
"linux": "Shift+Alt+F",
"macos": "Shift+Cmd+F"
},
"enabled": true
},
{
"id": "applyConfig",
"action": "applyJsonDraft",
"label": "Apply JSON config",
"keys": {
"windows": "Ctrl+S",
"linux": "Ctrl+S",
"macos": "Cmd+S"
},
"enabled": true
}
]
}
}
```
## Compatibility
- `version` is currently `1`.
- `formatter` is currently `sql-formatter`.
- `params` replacement is not executed by SQL formatting. Use `paramTypes` when parameter recognition is needed.
- Legacy templates that use `openReplacePanel` for the `replace` action are accepted and normalized on import.

View File

@ -296,18 +296,7 @@ test("normalizeEditorSettings keeps valid UI scales with two-decimal precision",
});
test("defaults SQL formatter settings", () => {
assert.deepEqual(DEFAULT_EDITOR_SETTINGS.sqlFormatter, {
keywordCase: "upper",
dataTypeCase: "preserve",
functionCase: "preserve",
useTabs: false,
tabWidth: 2,
logicalOperatorNewline: "before",
expressionWidth: 50,
linesBetweenQueries: 1,
denseOperators: false,
newlineBeforeSemicolon: false,
});
assert.deepEqual(DEFAULT_EDITOR_SETTINGS.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS);
assert.deepEqual(normalizeEditorSettings({}).sqlFormatter, DEFAULT_EDITOR_SETTINGS.sqlFormatter);
});
@ -328,6 +317,7 @@ test("normalizes saved SQL formatter settings", () => {
},
} as any).sqlFormatter,
{
...DEFAULT_SQL_FORMATTER_SETTINGS,
keywordCase: "lower",
functionCase: "upper",
dataTypeCase: "upper",

View File

@ -1,6 +1,8 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { DEFAULT_SQL_FORMATTER_SETTINGS, parseSqlFormatterConfig, serializeSqlFormatterConfig, normalizeSqlFormatterSettings, syncSqlFormatterConfigDraft, sqlFormatterOptions } from "../../apps/desktop/src/lib/sqlFormatterConfig.ts";
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, parseSqlFormatterConfig, serializeSqlFormatterConfig, sqlFormatterOptions, syncSqlFormatterConfigDraft, validateSqlFormatterEditorSettings } from "../../apps/desktop/src/lib/sqlFormatterConfig.ts";
const { editor: defaultEditorSettings, ...defaultOptionSettings } = DEFAULT_SQL_FORMATTER_SETTINGS;
test("normalizes empty formatter settings to defaults", () => {
assert.deepEqual(normalizeSqlFormatterSettings({}), DEFAULT_SQL_FORMATTER_SETTINGS);
@ -11,6 +13,8 @@ test("keeps valid formatter settings and clamps invalid values", () => {
keywordCase: "lower",
dataTypeCase: "upper",
functionCase: "lower",
identifierCase: "upper",
indentStyle: "tabularLeft",
useTabs: true,
tabWidth: 4,
logicalOperatorNewline: "after",
@ -18,12 +22,16 @@ test("keeps valid formatter settings and clamps invalid values", () => {
linesBetweenQueries: 2,
denseOperators: true,
newlineBeforeSemicolon: true,
paramTypes: { named: [":"], custom: [{ regex: "\\{\\w+\\}" }] },
});
assert.deepEqual(settings, {
...DEFAULT_SQL_FORMATTER_SETTINGS,
keywordCase: "lower",
dataTypeCase: "upper",
functionCase: "lower",
identifierCase: "upper",
indentStyle: "tabularLeft",
useTabs: true,
tabWidth: 4,
logicalOperatorNewline: "after",
@ -31,6 +39,7 @@ test("keeps valid formatter settings and clamps invalid values", () => {
linesBetweenQueries: 2,
denseOperators: true,
newlineBeforeSemicolon: true,
paramTypes: { named: [":"], custom: [{ regex: "\\{\\w+\\}" }] },
});
assert.deepEqual(
@ -38,6 +47,8 @@ test("keeps valid formatter settings and clamps invalid values", () => {
keywordCase: "camel",
dataTypeCase: "invalid",
functionCase: "invalid",
identifierCase: "invalid",
indentStyle: "wide",
useTabs: "yes",
tabWidth: 99,
logicalOperatorNewline: "middle",
@ -45,8 +56,16 @@ test("keeps valid formatter settings and clamps invalid values", () => {
linesBetweenQueries: 9,
denseOperators: "true",
newlineBeforeSemicolon: "false",
paramTypes: { named: ["#"] },
editor: { shortcuts: [{ id: "duplicateLine", keys: { windows: "Ctrl+W", linux: "Ctrl+W", macos: "Cmd+W" }, enabled: false }] },
}),
DEFAULT_SQL_FORMATTER_SETTINGS,
{
...DEFAULT_SQL_FORMATTER_SETTINGS,
editor: {
...defaultEditorSettings,
shortcuts: defaultEditorSettings.shortcuts.map((shortcut) => (shortcut.id === "duplicateLine" ? { ...shortcut, keys: { windows: "Ctrl+W", linux: "Ctrl+W", macos: "Cmd+W" }, enabled: false } : shortcut)),
},
},
);
});
@ -62,9 +81,10 @@ test("serializes formatter config as a stable versioned envelope", () => {
version: 1,
formatter: "sql-formatter",
options: {
...DEFAULT_SQL_FORMATTER_SETTINGS,
...defaultOptionSettings,
keywordCase: "lower",
},
editor: defaultEditorSettings,
},
null,
2,
@ -82,6 +102,8 @@ test("parses valid formatter config files", () => {
keywordCase: "lower",
functionCase: "upper",
dataTypeCase: "preserve",
identifierCase: "lower",
indentStyle: "tabularRight",
useTabs: false,
tabWidth: 4,
logicalOperatorNewline: "after",
@ -89,15 +111,24 @@ test("parses valid formatter config files", () => {
linesBetweenQueries: 0,
denseOperators: false,
newlineBeforeSemicolon: true,
paramTypes: { named: [":"], quoted: ["@"], positional: true },
},
editor: {
scope: "sqlFormatterConfigJsonEditor",
platforms: ["windows", "macos"],
shortcuts: [{ id: "duplicateLine", action: "copyLineDown", keys: { windows: "Ctrl+W", linux: "Ctrl+W", macos: "Cmd+W" }, enabled: true }],
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(result.settings, {
...DEFAULT_SQL_FORMATTER_SETTINGS,
keywordCase: "lower",
functionCase: "upper",
dataTypeCase: "preserve",
identifierCase: "lower",
indentStyle: "tabularRight",
useTabs: false,
tabWidth: 4,
logicalOperatorNewline: "after",
@ -105,9 +136,56 @@ test("parses valid formatter config files", () => {
linesBetweenQueries: 0,
denseOperators: false,
newlineBeforeSemicolon: true,
paramTypes: { named: [":"], quoted: ["@"], positional: true },
editor: {
...defaultEditorSettings,
platforms: ["windows", "macos"],
shortcuts: defaultEditorSettings.shortcuts.map((shortcut) => (shortcut.id === "duplicateLine" ? { ...shortcut, keys: { windows: "Ctrl+W", linux: "Ctrl+W", macos: "Cmd+W" } } : shortcut)),
},
});
});
test("parses user-facing editor shortcut actions as known shortcut ids", () => {
for (const action of ["replace", "openReplacePanel"]) {
const result = parseSqlFormatterConfig(
JSON.stringify({
version: 1,
formatter: "sql-formatter",
options: {},
editor: {
shortcuts: [{ id: "replace", action, keys: { windows: "Ctrl+R", linux: "Ctrl+R", macos: "Cmd+R" }, enabled: true }],
},
}),
);
assert.equal(result.ok, true);
if (result.ok) {
const shortcut = result.settings.editor.shortcuts.find((item) => item.id === "replace");
assert.equal(shortcut?.action, "openSearchPanel");
assert.deepEqual(shortcut?.keys, { windows: "Ctrl+R", linux: "Ctrl+R", macos: "Cmd+R" });
}
}
});
test("parses partial editor shortcut keys and fills missing platforms from defaults", () => {
const result = parseSqlFormatterConfig(
JSON.stringify({
version: 1,
formatter: "sql-formatter",
options: {},
editor: {
shortcuts: [{ id: "replace", keys: { windows: "Ctrl+R" }, enabled: true }],
},
}),
);
assert.equal(result.ok, true);
if (result.ok) {
const shortcut = result.settings.editor.shortcuts.find((item) => item.id === "replace");
assert.deepEqual(shortcut?.keys, { windows: "Ctrl+R", linux: "Ctrl+H", macos: "Cmd+Option+F" });
}
});
test("rejects malformed formatter config files", () => {
assert.deepEqual(parseSqlFormatterConfig("{bad json").ok, false);
assert.deepEqual(parseSqlFormatterConfig(JSON.stringify({ version: 2, formatter: "sql-formatter", options: {} })).ok, false);
@ -127,6 +205,68 @@ test("rejects invalid known formatter option values when parsing config files",
const invalidNumericChoice = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { tabWidth: 3 } }));
assert.equal(invalidNumericChoice.ok, false);
if (!invalidNumericChoice.ok) assert.match(invalidNumericChoice.message, /tabWidth/);
const invalidParams = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { params: ["42"] } }));
assert.equal(invalidParams.ok, false);
if (!invalidParams.ok) assert.match(invalidParams.message, /params/);
const legacyNullParams = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { params: null } }));
assert.equal(legacyNullParams.ok, true);
const invalidParamTypes = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { paramTypes: { custom: [{ regex: "" }] } } }));
assert.equal(invalidParamTypes.ok, false);
if (!invalidParamTypes.ok) assert.match(invalidParamTypes.message, /paramTypes/);
});
test("rejects invalid editor shortcut config", () => {
const unknownShortcut = parseSqlFormatterConfig(
JSON.stringify({
version: 1,
formatter: "sql-formatter",
options: {},
editor: { shortcuts: [{ id: "unknown", action: "copyLineDown", keys: { windows: "Ctrl+D", linux: "Ctrl+D", macos: "Cmd+D" }, enabled: true }] },
}),
);
assert.equal(unknownShortcut.ok, false);
if (!unknownShortcut.ok) assert.match(unknownShortcut.message, /unknown/);
const invalidKeys = parseSqlFormatterConfig(
JSON.stringify({
version: 1,
formatter: "sql-formatter",
options: {},
editor: { shortcuts: [{ id: "duplicateLine", action: "copyLineDown", keys: { windows: "", linux: "Ctrl+D", macos: "Cmd+D" }, enabled: true }] },
}),
);
assert.equal(invalidKeys.ok, false);
if (!invalidKeys.ok) assert.match(invalidKeys.message, /duplicateLine/);
const duplicateKeys = parseSqlFormatterConfig(
JSON.stringify({
version: 1,
formatter: "sql-formatter",
options: {},
editor: {
shortcuts: [
{ id: "find", action: "openSearchPanel", keys: { windows: "Ctrl+D", linux: "Ctrl+F", macos: "Cmd+F" }, enabled: true },
{ id: "duplicateLine", action: "copyLineDown", keys: { windows: "Ctrl+D", linux: "Ctrl+D", macos: "Cmd+D" }, enabled: true },
],
},
}),
);
assert.equal(duplicateKeys.ok, false);
if (!duplicateKeys.ok) assert.match(duplicateKeys.message, /Duplicate/);
});
test("validates editor shortcut settings outside config import", () => {
assert.deepEqual(validateSqlFormatterEditorSettings(DEFAULT_SQL_FORMATTER_SETTINGS.editor), { ok: true });
assert.equal(
validateSqlFormatterEditorSettings({
...DEFAULT_SQL_FORMATTER_SETTINGS.editor,
shortcuts: DEFAULT_SQL_FORMATTER_SETTINGS.editor.shortcuts.map((shortcut) => (shortcut.id === "duplicateLine" ? { ...shortcut, keys: { ...shortcut.keys, windows: "Ctrl+" } } : shortcut)),
}).ok,
false,
);
});
test("syncs valid JSON drafts so outer settings apply can persist them", () => {
@ -136,10 +276,11 @@ test("syncs valid JSON drafts so outer settings apply can persist them", () => {
version: 1,
formatter: "sql-formatter",
options: {
...DEFAULT_SQL_FORMATTER_SETTINGS,
...defaultOptionSettings,
keywordCase: "lower",
tabWidth: 4,
},
editor: defaultEditorSettings,
}),
(settings) => {
synced = settings;
@ -161,9 +302,10 @@ test("does not sync invalid JSON drafts", () => {
version: 1,
formatter: "sql-formatter",
options: {
...DEFAULT_SQL_FORMATTER_SETTINGS,
...defaultOptionSettings,
keywordCase: "camel",
},
editor: defaultEditorSettings,
}),
(settings) => {
synced = settings;
@ -186,6 +328,8 @@ test("maps DBX formatter settings to sql-formatter options", () => {
keywordCase: "lower",
dataTypeCase: "preserve",
functionCase: "preserve",
identifierCase: "preserve",
indentStyle: "standard",
useTabs: true,
tabWidth: 2,
logicalOperatorNewline: "before",
@ -195,4 +339,26 @@ test("maps DBX formatter settings to sql-formatter options", () => {
newlineBeforeSemicolon: true,
},
);
assert.deepEqual(
sqlFormatterOptions({
...DEFAULT_SQL_FORMATTER_SETTINGS,
paramTypes: { positional: true },
}),
{
keywordCase: "upper",
dataTypeCase: "preserve",
functionCase: "preserve",
identifierCase: "preserve",
indentStyle: "standard",
useTabs: false,
tabWidth: 2,
logicalOperatorNewline: "before",
expressionWidth: 50,
linesBetweenQueries: 1,
denseOperators: false,
newlineBeforeSemicolon: false,
paramTypes: { positional: true },
},
);
});

View File

@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { DEFAULT_SQL_FORMATTER_SETTINGS } from "../../apps/desktop/src/lib/sqlFormatterConfig.ts";
import { createSqlFormatterConfigKeymap, sqlFormatterConfigShortcutRows } from "../../apps/desktop/src/lib/sqlFormatterConfigEditor.ts";
const commands = {
@ -10,6 +11,9 @@ const commands = {
deleteLine: () => true,
moveLineUp: () => true,
moveLineDown: () => true,
undo: () => true,
redo: () => true,
selectAll: () => true,
openSearchPanel: () => true,
};
const actions = {
@ -18,39 +22,45 @@ const actions = {
};
const otherCommand = () => true;
function createBindings() {
return createSqlFormatterConfigKeymap(commands, actions);
function createBindings(editorSettings = DEFAULT_SQL_FORMATTER_SETTINGS.editor) {
return createSqlFormatterConfigKeymap(commands, actions, editorSettings);
}
test("builds common SQL formatter config editor key bindings", () => {
const bindings = createBindings();
assert.deepEqual(
bindings.map((binding) => binding.key),
["Tab", "Shift-Tab", "Mod-d", "Shift-Mod-k", "Alt-ArrowUp", "Alt-ArrowDown", "Shift-Alt-ArrowUp", "Shift-Alt-ArrowDown", "Ctrl-h", "Shift-Alt-f", "Mod-s"],
bindings.map((binding) => binding.win),
["Ctrl-f", "Ctrl-h", "Tab", "Shift-Tab", "Ctrl-d", "Ctrl-Shift-k", "Alt-ArrowUp", "Alt-ArrowDown", "Shift-Alt-ArrowUp", "Shift-Alt-ArrowDown", "Ctrl-z", "Ctrl-y", "Ctrl-a", "Shift-Alt-f", "Ctrl-s"],
);
assert.deepEqual(
bindings.map((binding) => binding.linux),
["Ctrl-f", "Ctrl-h", "Tab", "Shift-Tab", "Ctrl-d", "Ctrl-Shift-k", "Alt-ArrowUp", "Alt-ArrowDown", "Shift-Alt-ArrowUp", "Shift-Alt-ArrowDown", "Ctrl-z", "Ctrl-Shift-z", "Ctrl-a", "Shift-Alt-f", "Ctrl-s"],
);
});
test("sets search, format, and apply binding details", () => {
const bindings = createBindings();
const searchBinding = bindings.find((binding) => binding.key === "Ctrl-h");
const formatBinding = bindings.find((binding) => binding.key === "Shift-Alt-f");
const applyBinding = bindings.find((binding) => binding.key === "Mod-s");
const searchBinding = bindings.find((binding) => binding.win === "Ctrl-h");
const formatBinding = bindings.find((binding) => binding.win === "Shift-Alt-f");
const applyBinding = bindings.find((binding) => binding.win === "Ctrl-s");
assert.equal(searchBinding?.mac, "Mod-Alt-f");
assert.equal(searchBinding?.linux, "Ctrl-h");
assert.equal(searchBinding?.preventDefault, true);
assert.equal(searchBinding?.run, commands.openSearchPanel);
assert.equal(formatBinding?.mac, "Shift-Mod-f");
assert.equal(formatBinding?.preventDefault, true);
assert.equal(formatBinding?.run, actions.formatJson);
assert.equal(applyBinding?.mac, "Mod-s");
assert.equal(applyBinding?.preventDefault, true);
assert.equal(applyBinding?.run, actions.apply);
});
test("keeps duplicate line before later Mod-d search bindings", () => {
const bindings = [...createBindings(), { key: "Mod-d", run: otherCommand }];
const duplicateLineIndex = bindings.findIndex((binding) => binding.key === "Mod-d" && binding.run === commands.copyLineDown);
const searchKeymapIndex = bindings.findIndex((binding) => binding.key === "Mod-d" && binding.run === otherCommand);
test("keeps duplicate line before later Ctrl-d search bindings", () => {
const bindings = [...createBindings(), { win: "Ctrl-d", run: otherCommand }];
const duplicateLineIndex = bindings.findIndex((binding) => binding.win === "Ctrl-d" && binding.run === commands.copyLineDown);
const searchKeymapIndex = bindings.findIndex((binding) => binding.win === "Ctrl-d" && binding.run === otherCommand);
assert.notEqual(duplicateLineIndex, -1);
assert.notEqual(searchKeymapIndex, -1);
@ -58,6 +68,31 @@ test("keeps duplicate line before later Mod-d search bindings", () => {
assert.ok(duplicateLineIndex < searchKeymapIndex);
});
test("uses customized shortcut keys and skips disabled shortcuts", () => {
const editorSettings = {
...DEFAULT_SQL_FORMATTER_SETTINGS.editor,
shortcuts: DEFAULT_SQL_FORMATTER_SETTINGS.editor.shortcuts.map((shortcut) => {
if (shortcut.id === "duplicateLine") return { ...shortcut, keys: { windows: "Ctrl+W", linux: "Ctrl+W", macos: "Cmd+W" } };
if (shortcut.id === "deleteLine") return { ...shortcut, enabled: false };
return shortcut;
}),
};
const bindings = createBindings(editorSettings);
assert.equal(bindings.find((binding) => binding.win === "Ctrl-w")?.run, commands.copyLineDown);
assert.equal(bindings.find((binding) => binding.linux === "Ctrl-w")?.run, commands.copyLineDown);
assert.equal(bindings.some((binding) => binding.win === "Ctrl-Shift-k"), false);
});
test("honors enabled platforms when building key bindings", () => {
const bindings = createBindings({
...DEFAULT_SQL_FORMATTER_SETTINGS.editor,
platforms: ["linux"],
});
assert.equal(bindings.every((binding) => !binding.win && !binding.mac && !!binding.linux), true);
});
test("shows platform-aware shortcut labels", () => {
const windowsRows = sqlFormatterConfigShortcutRows("Win32");
const macRows = sqlFormatterConfigShortcutRows("MacIntel");
@ -66,6 +101,6 @@ test("shows platform-aware shortcut labels", () => {
assert.equal(macRows.find((row) => row.id === "duplicateLine")?.shortcut, "Cmd+D");
assert.equal(windowsRows.find((row) => row.id === "formatJson")?.shortcut, "Shift+Alt+F");
assert.equal(macRows.find((row) => row.id === "formatJson")?.shortcut, "Shift+Cmd+F");
assert.equal(windowsRows.find((row) => row.id === "apply")?.shortcut, "Ctrl+S");
assert.equal(macRows.find((row) => row.id === "apply")?.shortcut, "Cmd+S");
assert.equal(windowsRows.find((row) => row.id === "applyConfig")?.shortcut, "Ctrl+S");
assert.equal(macRows.find((row) => row.id === "applyConfig")?.shortcut, "Cmd+S");
});