From bcb2f7a624828ec2eb274622275299746fe592a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5FMr=E4=B8=B6s=20=E3=82=9B=F0=9F=98=81=C2=AE?= <31917567+ma123456mai@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:07:18 +0800 Subject: [PATCH] feat(format): default to dbx layout and align fields --- .../editor/SqlFormatterSettingsPanel.vue | 2 +- .../SqlFormatterLayoutStylePanel.spec.ts | 142 +++++++++++++----- ...bleStructureEditor.ddlTabLifecycle.spec.ts | 2 +- .../__tests__/useAppUpdater.spec.ts | 17 +++ apps/desktop/src/composables/useAppUpdater.ts | 8 +- .../sqlFormatterLayoutStyleParity.spec.ts | 4 +- apps/desktop/src/i18n/locales/az.ts | 2 +- apps/desktop/src/i18n/locales/en.ts | 2 +- apps/desktop/src/i18n/locales/es.ts | 2 +- apps/desktop/src/i18n/locales/id.ts | 2 +- apps/desktop/src/i18n/locales/it.ts | 2 +- apps/desktop/src/i18n/locales/ja.ts | 2 +- apps/desktop/src/i18n/locales/ko.ts | 2 +- apps/desktop/src/i18n/locales/pt-BR.ts | 2 +- apps/desktop/src/i18n/locales/ru.ts | 2 +- apps/desktop/src/i18n/locales/th.ts | 2 +- apps/desktop/src/i18n/locales/tr.ts | 2 +- apps/desktop/src/i18n/locales/zh-CN.ts | 2 +- apps/desktop/src/i18n/locales/zh-TW.ts | 2 +- .../lib/__tests__/sql/sqlFormatter.spec.ts | 43 ++++-- .../__tests__/sql/sqlFormatterConfig.spec.ts | 7 +- .../src/lib/__tests__/sql/sqlLayout.spec.ts | 35 ++++- .../src/lib/app/__tests__/appFonts.spec.ts | 10 +- apps/desktop/src/lib/app/appFonts.ts | 19 ++- apps/desktop/src/lib/sql/layout/internals.ts | 11 +- .../stores/__tests__/settingsStore.spec.ts | 33 ++++ apps/desktop/src/stores/settingsStore.ts | 12 +- apps/desktop/src/styles/tokens.css | 2 +- apps/desktop/vite.config.ts | 11 +- docs/content/docs/sql-formatter.cn.mdx | 6 +- docs/content/docs/sql-formatter.mdx | 6 +- packages/app-tests/sqlFormatter.test.ts | 5 +- 32 files changed, 303 insertions(+), 98 deletions(-) diff --git a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue index 40a529729d..28f206fa38 100644 --- a/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue +++ b/apps/desktop/src/components/editor/SqlFormatterSettingsPanel.vue @@ -102,8 +102,8 @@ const indentStyleOptions: { value: SqlFormatterIndentStyle; labelKey: string }[] ]; const layoutStyleOptions: { value: SqlFormatterLayoutStyle; labelKey: string }[] = [ - { value: "dbx", labelKey: "settings.sqlFormatterLayoutStyleDbx" }, { value: "classic", labelKey: "settings.sqlFormatterLayoutStyleClassic" }, + { value: "dbx", labelKey: "settings.sqlFormatterLayoutStyleDbx" }, ]; const tabWidthOptions: SqlFormatterTabWidth[] = [2, 4]; diff --git a/apps/desktop/src/components/editor/__tests__/SqlFormatterLayoutStylePanel.spec.ts b/apps/desktop/src/components/editor/__tests__/SqlFormatterLayoutStylePanel.spec.ts index 70b9d0c669..506fd66a84 100644 --- a/apps/desktop/src/components/editor/__tests__/SqlFormatterLayoutStylePanel.spec.ts +++ b/apps/desktop/src/components/editor/__tests__/SqlFormatterLayoutStylePanel.spec.ts @@ -1,47 +1,111 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; +// @vitest-environment happy-dom -/** - * Source-level guard for the layout-style control in the SQL formatter panel. - * Mounting the panel would pull in the CodeMirror JSON editor, so the one - * contract that matters for issue #11228 — the style select changes only the - * engine, never any other option — is pinned against the component source - * instead. - * - * There is deliberately no "apply classic preset" action: the user's decision - * was that choosing the classic mode is enough, and a second control that - * writes only part of what the old style needs reads as a partial application - * of it. Do not re-add one — the two option values v0.6.16 also changed are - * documented, not applied for the user. - * - * This assertion reads the component as text, so it depends on its current - * formatting: `functionBody` requires the closing brace at column 0 — re-run - * this spec after any formatter run, and keep comments inside that function - * free of a `word:` shape, which the key-set check also scans for. - */ -const source = readFileSync(new URL("../SqlFormatterSettingsPanel.vue", import.meta.url), "utf8"); +import { createApp, defineComponent, h, nextTick, type App } from "vue"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_SQL_FORMATTER_SETTINGS } from "@/lib/sql/sqlFormatterConfig"; -function functionBody(name: string): string { - const body = source.match(new RegExp(String.raw`function ${name}\([^)]*\) \{\n([\s\S]*?)\n\}`))?.[1]; - expect(body, `${name} not found in SqlFormatterSettingsPanel.vue`).toBeTruthy(); - return body!; -} +vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) })); +vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) })); +vi.mock("@/lib/common/clipboard", () => ({ copyToClipboard: vi.fn() })); +vi.mock("@/lib/export/saveTextFile", () => ({ saveTextFile: vi.fn() })); +vi.mock("@/lib/editor/codemirrorSearchKeymap", () => ({ searchKeymapWithoutModD: vi.fn() })); -describe("SQL formatter layout style panel wiring", () => { - it("writes only layoutStyle when the style select changes", () => { - const body = functionBody("onLayoutStyle"); +vi.mock("@lucide/vue", async () => { + const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness"); + const icon = createPassthroughStub("Icon", "i"); + return { Upload: icon, Download: icon, RotateCcw: icon, WandSparkles: icon, Save: icon, Copy: icon }; +}); - expect(body).toContain('updateOption("layoutStyle", value)'); - expect(body).not.toContain("expressionWidth"); - expect(body).not.toContain("fromClauseLayout"); +vi.mock("@/components/ui/button", async () => ({ + Button: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Button", "button"), +})); +vi.mock("@/components/ui/label", async () => ({ + Label: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Label", "label"), +})); +vi.mock("@/components/ui/select", async () => { + const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness"); + const Select = defineComponent({ + name: "Select", + props: { modelValue: { type: [String, Number], default: "" } }, + emits: ["update:modelValue"], + setup(props, { emit, slots }) { + return () => + h( + "div", + { + "data-stub": "Select", + "data-model-value": String(props.modelValue), + onPick: (event: CustomEvent<{ value: string }>) => emit("update:modelValue", event.detail.value), + }, + [slots.default?.()], + ); + }, + }); + return { + Select, + SelectContent: createPassthroughStub("SelectContent"), + SelectItem: createPassthroughStub("SelectItem", "button"), + SelectTrigger: createPassthroughStub("SelectTrigger", "button"), + SelectValue: createPassthroughStub("SelectValue", "span"), + }; +}); +vi.mock("@/components/ui/switch", async () => ({ + Switch: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Switch", "input"), +})); +vi.mock("@/components/ui/tabs", async () => { + const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness"); + return { + Tabs: createPassthroughStub("Tabs"), + TabsContent: createPassthroughStub("TabsContent"), + TabsList: createPassthroughStub("TabsList"), + TabsTrigger: createPassthroughStub("TabsTrigger", "button"), + }; +}); + +import SqlFormatterSettingsPanel from "@/components/editor/SqlFormatterSettingsPanel.vue"; + +describe("SQL formatter layout style panel", () => { + let app: App | null = null; + let host: HTMLDivElement | null = null; + + function mountPanel(modelValue: unknown, update: ReturnType) { + host = document.createElement("div"); + document.body.append(host); + app = createApp(SqlFormatterSettingsPanel, { modelValue, "onUpdate:modelValue": update }); + app.mount(host); + return host; + } + + afterEach(() => { + app?.unmount(); + app = null; + host?.remove(); + host = null; }); - it("offers both layout styles and no preset action", () => { - expect(source).toContain('{ value: "dbx", labelKey: "settings.sqlFormatterLayoutStyleDbx" }'); - expect(source).toContain('{ value: "classic", labelKey: "settings.sqlFormatterLayoutStyleClassic" }'); - // One control only: the classic mode is chosen here, and nothing writes the - // option values it used to imply. DEFAULT_SQL_FORMATTER_SETTINGS already is - // the dbx end state, so Restore Defaults covers the other direction. - expect(source).not.toContain("ApplyClassicPreset"); + it("selects the DBX layout by default and preserves other formatter settings", async () => { + const update = vi.fn(); + const root = mountPanel(DEFAULT_SQL_FORMATTER_SETTINGS, update); + + const styleSelect = root.querySelector('[data-model-value="dbx"]'); + expect(styleSelect).not.toBeNull(); + const layoutOptions = Array.from(styleSelect!.querySelectorAll('[data-stub="SelectItem"]')).map((node) => node.getAttribute("value")); + expect(layoutOptions).toEqual(["classic", "dbx"]); + + styleSelect!.dispatchEvent(new CustomEvent("pick", { detail: { value: "classic" } })); + await nextTick(); + expect(update).toHaveBeenLastCalledWith({ ...DEFAULT_SQL_FORMATTER_SETTINGS, layoutStyle: "classic" }); + }); + + it("restores the current formatter defaults", async () => { + const update = vi.fn(); + const root = mountPanel({ ...DEFAULT_SQL_FORMATTER_SETTINGS, layoutStyle: "dbx", expressionWidth: 50 }, update); + + const restoreButton = Array.from(root.querySelectorAll("button")).find((node) => node.textContent?.includes("settings.sqlFormatterRestoreDefaults")); + expect(restoreButton).toBeDefined(); + restoreButton!.click(); + await nextTick(); + + expect(update).toHaveBeenCalledWith(DEFAULT_SQL_FORMATTER_SETTINGS); }); }); diff --git a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts index a4ff65396d..d0caf279e2 100644 --- a/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts +++ b/apps/desktop/src/components/structure/__tests__/TableStructureEditor.ddlTabLifecycle.spec.ts @@ -269,7 +269,7 @@ describe("TableStructureEditor DDL tab lifecycle", () => { app.mount(root); const content = () => root.querySelector(".cm-content")?.textContent ?? ""; const toggle = () => root.querySelector('[role="switch"]')!; - await vi.waitFor(() => expect(content()).toContain("PARTITION BY HASH")); + await vi.waitFor(() => expect(content()).toMatch(/PARTITION BY\s+HASH/)); expect(content()).not.toContain("PCTFREE"); expect(toggle().getAttribute("aria-checked")).toBe("true"); toggle().click(); diff --git a/apps/desktop/src/composables/__tests__/useAppUpdater.spec.ts b/apps/desktop/src/composables/__tests__/useAppUpdater.spec.ts index 1b465e3efc..a625d7448c 100644 --- a/apps/desktop/src/composables/__tests__/useAppUpdater.spec.ts +++ b/apps/desktop/src/composables/__tests__/useAppUpdater.spec.ts @@ -55,6 +55,7 @@ async function flush() { } beforeEach(() => { vi.resetAllMocks(); + vi.stubEnv("VITE_DBX_DISABLE_UPDATER", ""); settings.updateDownloadSource = "official"; settings.ignoredUpdateVersion = ""; settings.updateNotificationsEnabled = true; @@ -70,6 +71,7 @@ beforeEach(() => { afterEach(() => { app?.unmount(); vi.useRealTimers(); + vi.unstubAllEnvs(); }); describe("older version release pages", () => { it("maps a historical version to the matching GitHub release page", () => { @@ -89,6 +91,21 @@ describe("older version release pages", () => { }); describe("silent update lifecycle", () => { + it("does not check or download updates when disabled for an isolated dev client", async () => { + vi.stubEnv("VITE_DBX_DISABLE_UPDATER", "1"); + const updater = mount(); + await updater.initialize(); + await flush(); + await updater.checkUpdates(); + await updater.downloadUpdateInBackground(); + await updater.installDownloadedUpdate(); + + expect(mocks.getDownloadedUpdate).not.toHaveBeenCalled(); + expect(mocks.checkForUpdates).not.toHaveBeenCalled(); + expect(mocks.downloadUpdate).not.toHaveBeenCalled(); + expect(mocks.installDownloadedUpdate).not.toHaveBeenCalled(); + }); + it("checks and shows a badge without downloading when automatic downloads are disabled", async () => { settings.autoUpdateApp = false; const updater = mount(); diff --git a/apps/desktop/src/composables/useAppUpdater.ts b/apps/desktop/src/composables/useAppUpdater.ts index 86dd05d8b8..0a52f3610e 100644 --- a/apps/desktop/src/composables/useAppUpdater.ts +++ b/apps/desktop/src/composables/useAppUpdater.ts @@ -131,6 +131,7 @@ export async function resolveUpdaterProxy(): Promise { export function useAppUpdater(options: UseAppUpdaterOptions = {}) { const { t } = useI18n(); const settingsStore = useSettingsStore(); + const updaterDisabled = import.meta.env.VITE_DBX_DISABLE_UPDATER === "1"; type Phase = "idle" | "checking" | "downloading" | "ready" | "preparing" | "installing" | "restart"; const phase = ref("idle"); const downloaded = ref(null); @@ -223,7 +224,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) { return t("updates.installFailed", { error: message }); } async function checkUpdates(checkOptions: { silent?: boolean } = {}) { - if (disposed || isIgnoringUpdate.value) return; + if (updaterDisabled || disposed || isIgnoringUpdate.value) return; if (!checkOptions.silent) showUpdateDialog.value = true; // A downloaded-but-uninstalled update keeps the app in the ready phase; checks // continue so a newer release can replace the cached package. @@ -281,7 +282,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) { } } async function downloadUpdateInBackground(automatic = false) { - if (disposed || isIgnoringUpdate.value || phase.value !== "idle" || downloaded.value || !canDownloadAndInstallUpdate(updateInfo.value, isTauriRuntime())) return; + if (updaterDisabled || disposed || isIgnoringUpdate.value || phase.value !== "idle" || downloaded.value || !canDownloadAndInstallUpdate(updateInfo.value, isTauriRuntime())) return; if (automatic && !autoUpdateEnabled.value) return; automaticDownload = automatic; const version = updateInfo.value!.latest_version; @@ -434,14 +435,17 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) { } } async function installDownloadedUpdate() { + if (updaterDisabled) return; await performInstall(false); } async function restartApp() { + if (updaterDisabled) return; if (updateReady.value) await performInstall(true); } async function initialize() { if (initialized || disposed) return; initialized = true; + if (updaterDisabled) return; if (isTauriRuntime()) { phase.value = "checking"; try { diff --git a/apps/desktop/src/i18n/__tests__/sqlFormatterLayoutStyleParity.spec.ts b/apps/desktop/src/i18n/__tests__/sqlFormatterLayoutStyleParity.spec.ts index 22e5c6a0aa..ff6a1a8acf 100644 --- a/apps/desktop/src/i18n/__tests__/sqlFormatterLayoutStyleParity.spec.ts +++ b/apps/desktop/src/i18n/__tests__/sqlFormatterLayoutStyleParity.spec.ts @@ -8,7 +8,7 @@ import { readFileSync } from "node:fs"; * `en` under the locale at import time, so asserting on the imported modules * would report the key as present even after a locale drops it. */ -const LOCALE_FILES = ["az", "en", "es", "id", "it", "ja", "ko", "pt-BR", "ru", "tr", "zh-CN", "zh-TW"]; +const LOCALE_FILES = ["az", "en", "es", "id", "it", "ja", "ko", "pt-BR", "ru", "th", "tr", "zh-CN", "zh-TW"]; const LAYOUT_STYLE_KEYS = ["sqlFormatterLayoutStyle", "sqlFormatterLayoutStyleDbx", "sqlFormatterLayoutStyleClassic"]; @@ -41,7 +41,7 @@ describe("SQL formatter layout style i18n parity", () => { const source = localeSource("en"); expect(declaredString(source, "sqlFormatterLayoutStyle")).toBe("Layout style"); - expect(declaredString(source, "sqlFormatterLayoutStyleDbx")).toBe("DBX (default)"); + expect(declaredString(source, "sqlFormatterLayoutStyleDbx")).toBe("DBX"); expect(declaredString(source, "sqlFormatterLayoutStyleClassic")).toBe("Classic"); }); diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 7a000c47db..4a24711110 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -7289,7 +7289,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Sola düzülmüş cədvəl", sqlFormatterIndentStyleTabularRight: "Sağa düzülmüş cədvəl", sqlFormatterLayoutStyle: "Düzən üslubu", - sqlFormatterLayoutStyleDbx: "DBX (standart)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Klassik", sqlFormatterLogicalOperatorNewline: "Məntiqi operator üçün yeni sətir", sqlFormatterLogicalBefore: "Operatordan əvvəl", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 54a2220af6..dce526d084 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -8707,7 +8707,7 @@ export default { sqlFormatterIndentStyleTabularLeft: "Tabular left", sqlFormatterIndentStyleTabularRight: "Tabular right", sqlFormatterLayoutStyle: "Layout style", - sqlFormatterLayoutStyleDbx: "DBX (default)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Classic", sqlFormatterLogicalOperatorNewline: "Logical operator newline", sqlFormatterLogicalBefore: "Before operator", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 138ab135d4..b79f3f0fdd 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -7822,7 +7822,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Tabular izquierda", sqlFormatterIndentStyleTabularRight: "Tabular derecha", sqlFormatterLayoutStyle: "Estilo de diseño", - sqlFormatterLayoutStyleDbx: "DBX (predeterminado)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Clásico", sqlFormatterLogicalOperatorNewline: "Salto de línea en operador lógico", sqlFormatterLogicalBefore: "Antes del operador", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 784426f441..2c16b34383 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -8031,7 +8031,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Tabular rata kiri", sqlFormatterIndentStyleTabularRight: "Tabular rata kanan", sqlFormatterLayoutStyle: "Gaya tata letak", - sqlFormatterLayoutStyleDbx: "DBX (bawaan)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Klasik", sqlFormatterLogicalOperatorNewline: "Baris baru pada operator logika", sqlFormatterLogicalBefore: "Sebelum operator", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index e106eef8f1..faca3f37f8 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -7601,7 +7601,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Tabellare sinistra", sqlFormatterIndentStyleTabularRight: "Tabellare destra", sqlFormatterLayoutStyle: "Stile di impaginazione", - sqlFormatterLayoutStyleDbx: "DBX (predefinito)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Classico", sqlFormatterLogicalOperatorNewline: "Nuova riga operatore logico", sqlFormatterLogicalBefore: "Prima dell'operatore", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 220baac0bc..17ca9cc4f3 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -7629,7 +7629,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "表形式左寄せ", sqlFormatterIndentStyleTabularRight: "表形式右寄せ", sqlFormatterLayoutStyle: "レイアウトスタイル", - sqlFormatterLayoutStyleDbx: "DBX(デフォルト)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "クラシック", sqlFormatterLogicalOperatorNewline: "論理演算子の改行", sqlFormatterLogicalBefore: "演算子の前", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index df564983fb..76f70e3b33 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -7471,7 +7471,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "표 왼쪽 정렬", sqlFormatterIndentStyleTabularRight: "표 오른쪽 정렬", sqlFormatterLayoutStyle: "레이아웃 스타일", - sqlFormatterLayoutStyleDbx: "DBX(기본값)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "클래식", sqlFormatterLogicalOperatorNewline: "논리 연산자 줄바꿈", sqlFormatterLogicalBefore: "연산자 앞", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 97a186bec9..a282579259 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -7600,7 +7600,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Tabular esquerda", sqlFormatterIndentStyleTabularRight: "Tabular direita", sqlFormatterLayoutStyle: "Estilo de layout", - sqlFormatterLayoutStyleDbx: "DBX (padrão)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Clássico", sqlFormatterLogicalOperatorNewline: "Quebra de linha no operador lógico", sqlFormatterLogicalBefore: "Antes do operador", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index f210a0f368..0898f95d14 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -8915,7 +8915,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Табличный слева", sqlFormatterIndentStyleTabularRight: "Табличный справа", sqlFormatterLayoutStyle: "Стиль разметки", - sqlFormatterLayoutStyleDbx: "DBX (по умолчанию)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Классический", sqlFormatterLogicalOperatorNewline: "Перенос логических операторов", sqlFormatterLogicalBefore: "Перед оператором", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index b6aca0fe34..a45de22cb9 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -9363,7 +9363,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "จัดตารางชิดซ้าย", sqlFormatterIndentStyleTabularRight: "จัดตารางชิดขวา", sqlFormatterLayoutStyle: "รูปแบบ Layout", - sqlFormatterLayoutStyleDbx: "DBX (ค่าเริ่มต้น)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "คลาสสิก", sqlFormatterLogicalOperatorNewline: "ขึ้นบรรทัดใหม่ของตัวดำเนินการตรรกะ", sqlFormatterLogicalBefore: "ก่อนตัวดำเนินการ", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 7605f10089..e009e5cb87 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7170,7 +7170,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "Tablo biçiminde sola", sqlFormatterIndentStyleTabularRight: "Tablo biçiminde sağa", sqlFormatterLayoutStyle: "Düzen biçemi", - sqlFormatterLayoutStyleDbx: "DBX (varsayılan)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "Klasik", sqlFormatterLogicalOperatorNewline: "Mantıksal işleçte satır sonu", sqlFormatterLogicalBefore: "İşleçten önce", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 53c575cf1b..5c8f4000ac 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -8689,7 +8689,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "表格式左对齐", sqlFormatterIndentStyleTabularRight: "表格式右对齐", sqlFormatterLayoutStyle: "排版风格", - sqlFormatterLayoutStyleDbx: "DBX(默认)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "经典", sqlFormatterLogicalOperatorNewline: "逻辑运算符换行", sqlFormatterLogicalBefore: "运算符前换行", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index ebc870eab5..63c9a989b2 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -6950,7 +6950,7 @@ export default withEnglishFallback({ sqlFormatterIndentStyleTabularLeft: "表格式靠左", sqlFormatterIndentStyleTabularRight: "表格式靠右", sqlFormatterLayoutStyle: "排版風格", - sqlFormatterLayoutStyleDbx: "DBX(預設)", + sqlFormatterLayoutStyleDbx: "DBX", sqlFormatterLayoutStyleClassic: "經典", sqlFormatterLogicalOperatorNewline: "邏輯運算子換行", sqlFormatterLogicalBefore: "運算子前換行", diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts index 8a05897f2e..a3703f8f9f 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts @@ -1,8 +1,29 @@ import { describe, expect, it } from "vitest"; -import { canFormatSqlForDatabaseType, compressSqlText, formatSqlForDisplay, formatSqlForEditing, formatSqlText, MAX_SQL_FORMAT_CHARS, sqlFormatDialectForDbType, UnsupportedStructuredInputError } from "@/lib/sql/sqlFormatter"; +import { + canFormatSqlForDatabaseType, + compressSqlText, + formatSqlForDisplay as formatSqlForDisplayWithBuildDefaults, + formatSqlForEditing as formatSqlForEditingWithBuildDefaults, + formatSqlText as formatSqlTextWithBuildDefaults, + MAX_SQL_FORMAT_CHARS, + sqlFormatDialectForDbType, + UnsupportedStructuredInputError, +} from "@/lib/sql/sqlFormatter"; +import type { SqlFormatDialect } from "@/lib/sql/sqlFormatter"; import type { SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; import { extractSqlParameters } from "@/lib/sql/sqlParameters"; +// Most cases in this regression suite pin the former compact layout while +// checking dialect-specific token handling. New default-layout behavior is +// exercised explicitly with the build-default aliases below. +function withDbxLayout(settings: Partial = {}): Partial { + return { layoutStyle: "dbx", ...settings }; +} + +const formatSqlText = (sql: string, dialect: SqlFormatDialect = "generic", settings: Partial = {}) => formatSqlTextWithBuildDefaults(sql, dialect, withDbxLayout(settings)); +const formatSqlForEditing = (sql: string, dialect: SqlFormatDialect = "generic", settings: Partial = {}) => formatSqlForEditingWithBuildDefaults(sql, dialect, withDbxLayout(settings)); +const formatSqlForDisplay = (sql: string, dialect: SqlFormatDialect = "generic", settings: Partial = {}) => formatSqlForDisplayWithBuildDefaults(sql, dialect, withDbxLayout(settings)); + describe("Neo4j Cypher formatting", () => { const reproduction = `MATCH (a:LibraryDomainOrganization) WHERE a.id= $id CALL apoc.path.expandConfig(a, {uniqueness:'RELATIONSHIP_GLOBAL', minLevel:0, maxLevel:-1, relationshipFilter:'HasLibrary>|HasLibraryParameter>' }) YIELD path UNWIND relationships(path) as r WITH DISTINCT r WITH r, startNode(r) as s, endNode(r) as e OPTIONAL MATCH (s)-[:panda_data_master_version_rel]-(so) OPTIONAL MATCH (e)-[:panda_data_master_version_rel]-(eo) WITH r, CASE WHEN s.classType = 'COMMON' OR s.classType = 'INTERFACE' THEN s WHEN type(r) = 'panda_data_master_version_rel' THEN null ELSE so END as sm OPTIONAL MATCH (sm)-[permission:panda_dataPermission]->(target) WHERE target:panda_member AND target.id IN $memberIds AND permission.permissionType IN ['OWNER','ACTIVE','PASSIVE'] RETURN type(r) as relationType, sm, [s, e] as endpoints`; @@ -779,24 +800,26 @@ describe("classic layout style (v0.6.15 reproduction)", () => { await expect(formatSqlText(sql, "oracle", { ...classic, functionCase: "upper" })).resolves.toBe(lines("SELECT", " SUBSTRB(NAME, 1, 3),", " INSTR2(NAME, 'x'),", " LENGTHB(NAME)", "FROM", " t;")); }); - it("keeps DBX's own printer as the default and for the explicit dbx style", async () => { + it("uses the DBX layout by default and keeps classic selectable", async () => { const sql = "select * from task order by created_at desc;"; const collapsed = "SELECT * FROM task ORDER BY created_at DESC;"; - // Same input, opposite layouts: the collapsed form is the DBX printer's, - // so this proves the routing split rather than the goldens above. - expect(await formatSqlText(sql, "mysql", { layoutStyle: "dbx" })).toBe(collapsed); - expect(await formatSqlText(sql, "mysql")).toBe(collapsed); - // A settings object saved before `layoutStyle` existed normalizes to dbx. - expect(await formatSqlText(sql, "mysql", {} as Partial)).toBe(collapsed); + expect(await formatSqlTextWithBuildDefaults(sql, "mysql")).toBe(collapsed); + expect(await formatSqlTextWithBuildDefaults(sql, "mysql", {} as Partial)).toBe(collapsed); + expect(await formatSqlTextWithBuildDefaults(sql, "mysql", { layoutStyle: "classic" })).toBe("SELECT\n *\nFROM task\nORDER BY\n created_at DESC;"); + }); + + it("formats multiple statements on separate blocks by default", async () => { + const sql = "select id, name from users; select count(*) from orders;"; + + await expect(formatSqlTextWithBuildDefaults(sql, "mysql")).resolves.toBe("SELECT id,\n name\nFROM users;\n\nSELECT count(*) FROM orders;"); }); it("merges a classic FROM clause onto its source with a single space under the standard indent", async () => { // `sameLine` (the default) merges the clause line with the source line, and // the merge must not reuse the source's indent as the gap: that is the // tabular styles' alignment column, but under the standard indent it would - // read `FROM t`. Selecting classic is now the only path to this merge, so - // the classic style is what pins its appearance. + // read `FROM t`. This explicitly exercises sql-formatter's upstream layout. expect(await formatSqlText("select * from t", "mysql", classic)).toBe(lines("SELECT", " *", "FROM", " t")); expect(await formatSqlText("select * from t", "mysql", { ...classic, fromClauseLayout: "sameLine" })).toBe(lines("SELECT", " *", "FROM t")); }); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts index 73a417b315..1103b5d8cb 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatterConfig.spec.ts @@ -91,12 +91,12 @@ describe("sqlFormatterConfig shortcut storage", () => { }); describe("sqlFormatterConfig layout style", () => { - it("defaults to the DBX layout and keeps it out of the sql-formatter options", () => { + it("defaults to the DBX layout and keeps the style out of sql-formatter options", () => { const config = JSON.parse(serializeSqlFormatterConfig({})); expect(config.options.layoutStyle).toBe("dbx"); // layoutStyle is DBX-private: handing it to sql-formatter would throw. - expect("layoutStyle" in sqlFormatterOptions({ layoutStyle: "classic" })).toBe(false); + expect("layoutStyle" in sqlFormatterOptions({ layoutStyle: "dbx" })).toBe(false); }); it("round-trips the classic layout style", () => { @@ -115,8 +115,7 @@ describe("sqlFormatterConfig layout style", () => { }); it("accepts both config versions and rejects anything else", () => { - // Version 1 predates layoutStyle; absence normalizes to the current - // default and must never be read as a historical choice of classic. + // Version 1 predates layoutStyle; absence normalizes to the current default. const version1 = parseSqlFormatterConfig(JSON.stringify({ version: 1, formatter: "sql-formatter", options: { keywordCase: "lower" } })); expect(version1).toEqual(expect.objectContaining({ ok: true })); if (version1.ok) { diff --git a/apps/desktop/src/lib/__tests__/sql/sqlLayout.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlLayout.spec.ts index de750cd6b2..37c6b3b581 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlLayout.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlLayout.spec.ts @@ -3,7 +3,7 @@ import { formatSqlText } from "@/lib/sql/sqlFormatter"; import { DEFAULT_SQL_FORMATTER_SETTINGS, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; /** - * Golden tests for the layout engine behind DBX's default formatting style. + * Golden tests for DBX's default layout. * The expectations for the queries in issues #827, #4850 and #5170 pin the * real-world shapes those reports asked for; the rest pin the rules those * examples exercise — the collapse of short statements, the wrap of long ones, @@ -212,6 +212,39 @@ describe("sql layout", () => { expect(await format("SELECT a, bb AS x, c AS yyy FROM t;")).toBe(lines("SELECT a,", " bb AS x,", " c AS yyy", "FROM t;")); }); + it("keeps DBX JOIN and condition layout while aligning aliases and FROM", async () => { + const sql = lines( + "SELECT js.start_time AS jobslipstarttime, js.end_time AS jobslipendtime, js.job_area_ids AS jobareaids, jsm.job_slip_id, jsm.flag, upi.worker_basic_info_id AS workerid", + "FROM loc_job_slip js LEFT JOIN loc_job_slip_audit jsa ON js.id = jsa.job_slip_id LEFT JOIN loc_job_slip_manager jsm ON js.id = jsm.job_slip_id LEFT JOIN loc_user_proj_info upi ON upi.bp_user_id = jsm.user_id", + "WHERE js.deleted = 0 AND jsa.deleted = 0 AND jsm.deleted = 0;", + ); + + const expected = lines( + "SELECT js.start_time AS jobslipstarttime,", + " js.end_time AS jobslipendtime,", + " js.job_area_ids AS jobareaids,", + " jsm.job_slip_id,", + " jsm.flag,", + " upi.worker_basic_info_id AS workerid", + "FROM loc_job_slip js", + " LEFT JOIN loc_job_slip_audit jsa ON js.id = jsa.job_slip_id", + " LEFT JOIN loc_job_slip_manager jsm ON js.id = jsm.job_slip_id", + " LEFT JOIN loc_user_proj_info upi ON upi.bp_user_id = jsm.user_id", + "WHERE js.deleted = 0", + " AND jsa.deleted = 0", + " AND jsm.deleted = 0;", + ); + + const formatted = await format(sql); + expect(formatted).toBe(expected); + expect(await formatSqlText(sql, "mysql")).toBe(expected); + + const projection = formatted.split("\n").slice(0, 6); + const fieldStartColumns = projection.map((line, index) => (index === 0 ? line.indexOf("js.start_time") : line.search(/\S/))); + expect(new Set(fieldStartColumns)).toEqual(new Set([7])); + expect(new Set(projection.filter((line) => line.includes(" AS ")).map((line) => line.indexOf("AS"))).size).toBe(1); + }); + it("preserves comments and width wrapping inside CASE expressions", async () => { const commented = await format(lines("SELECT id, CASE WHEN a = 1 THEN 'one' -- retain this", "ELSE 'other' END AS label FROM t;")); expect(commented).toContain("-- retain this\n"); diff --git a/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts b/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts index f4266de9d7..d168f8b0dd 100644 --- a/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts +++ b/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts @@ -7,7 +7,13 @@ describe("appFonts", () => { expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("PingFang SC"); expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("Hiragino Sans GB"); expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("Microsoft YaHei"); - expect(DEFAULT_MONO_FALLBACK_FONTS).toMatch(/monospace$/); + expect(DEFAULT_MONO_FALLBACK_FONTS.indexOf("monospace")).toBeLessThan(DEFAULT_MONO_FALLBACK_FONTS.indexOf('"PingFang SC"')); + }); + + it("keeps the system fallback monospaced before proportional CJK fonts", () => { + expect(DEFAULT_MONO_FALLBACK_FONTS).toMatch(/^ui-monospace/); + expect(DEFAULT_MONO_FALLBACK_FONTS.indexOf("monospace")).toBeGreaterThanOrEqual(0); + expect(DEFAULT_MONO_FALLBACK_FONTS.indexOf("monospace")).toBeLessThan(DEFAULT_MONO_FALLBACK_FONTS.indexOf('"Microsoft YaHei"')); }); it("appends CJK fallbacks to DEFAULT_MONO_FONT_FAMILY", () => { @@ -23,8 +29,10 @@ describe("appFonts", () => { it("maps legacy preset values to updated stacks with CJK fallbacks", () => { expect(LEGACY_PRESET_FONT_MAP["'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace"]).toBe(DEFAULT_MONO_FONT_FAMILY); + expect(LEGACY_PRESET_FONT_MAP[`'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`]).toBe(DEFAULT_MONO_FONT_FAMILY); expect(LEGACY_PRESET_FONT_MAP["'JetBrains Mono', 'Fira Code', monospace"]).toContain(DEFAULT_MONO_FALLBACK_FONTS); expect(LEGACY_PRESET_FONT_MAP["'Consolas', 'Courier New', monospace"]).toContain(DEFAULT_MONO_FALLBACK_FONTS); + expect(LEGACY_PRESET_FONT_MAP[`"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`]).toBe(DEFAULT_MONO_FALLBACK_FONTS); expect(LEGACY_PRESET_FONT_MAP["monospace"]).toBe(DEFAULT_MONO_FALLBACK_FONTS); }); diff --git a/apps/desktop/src/lib/app/appFonts.ts b/apps/desktop/src/lib/app/appFonts.ts index 7c487bbcb4..c29b1fb967 100644 --- a/apps/desktop/src/lib/app/appFonts.ts +++ b/apps/desktop/src/lib/app/appFonts.ts @@ -4,10 +4,12 @@ export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family"; export const DATA_GRID_FONT_FAMILY_CSS_VAR = "--dbx-data-grid-font-family"; export const DEFAULT_UI_FONT_FAMILY = `"Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif`; -// CJK and system fallback fonts appended to monospace font stacks so Chinese/CJK -// glyphs fall back smoothly to sans-serif CJK fonts (e.g. PingFang SC on macOS, -// Microsoft YaHei on Windows) instead of ugly SimSun/NSimSun or bitmap fonts (dbx#674, dbx#1510). -export const DEFAULT_MONO_FALLBACK_FONTS = `"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`; +// Keep the generic monospace family before CJK fallbacks. CSS chooses a font +// per glyph, so placing PingFang/Microsoft YaHei first also makes ASCII SQL +// render proportionally when the named coding fonts are unavailable. CJK +// glyphs still fall through to these fonts when the monospace face lacks them +// (dbx#674, dbx#1510). +export const DEFAULT_MONO_FALLBACK_FONTS = `ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei"`; // Keep in sync with the editor font default in settingsStore; also the baseline // `--font-mono` value in tokens.css, which plugin sandboxes consume via the bridge. @@ -27,14 +29,23 @@ export const FONT_FAMILIES: { value: string; label: string }[] = [ { value: DEFAULT_MONO_FALLBACK_FONTS, label: "System Monospace" }, ]; +const LEGACY_MONO_CJK_FALLBACK_FONTS = `"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`; + export const LEGACY_PRESET_FONT_MAP: Record = { "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace": DEFAULT_MONO_FONT_FAMILY, + [`'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: DEFAULT_MONO_FONT_FAMILY, "'JetBrains Mono', 'Fira Code', monospace": `'JetBrains Mono', 'Fira Code', ${DEFAULT_MONO_FALLBACK_FONTS}`, + [`'JetBrains Mono', 'Fira Code', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: `'JetBrains Mono', 'Fira Code', ${DEFAULT_MONO_FALLBACK_FONTS}`, "'Cascadia Code', 'Cascadia Mono', monospace": `'Cascadia Code', 'Cascadia Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`, + [`'Cascadia Code', 'Cascadia Mono', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: `'Cascadia Code', 'Cascadia Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`, "'Source Code Pro', monospace": `'Source Code Pro', ${DEFAULT_MONO_FALLBACK_FONTS}`, + [`'Source Code Pro', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: `'Source Code Pro', ${DEFAULT_MONO_FALLBACK_FONTS}`, "'SF Mono', 'Menlo', monospace": `'SF Mono', 'Menlo', ${DEFAULT_MONO_FALLBACK_FONTS}`, + [`'SF Mono', 'Menlo', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: `'SF Mono', 'Menlo', ${DEFAULT_MONO_FALLBACK_FONTS}`, "'Consolas', 'Courier New', monospace": `'Consolas', 'Courier New', ${DEFAULT_MONO_FALLBACK_FONTS}`, + [`'Consolas', 'Courier New', ${LEGACY_MONO_CJK_FALLBACK_FONTS}`]: `'Consolas', 'Courier New', ${DEFAULT_MONO_FALLBACK_FONTS}`, monospace: DEFAULT_MONO_FALLBACK_FONTS, + [LEGACY_MONO_CJK_FALLBACK_FONTS]: DEFAULT_MONO_FALLBACK_FONTS, }; export function cssFontFamilyForName(name: string): string { diff --git a/apps/desktop/src/lib/sql/layout/internals.ts b/apps/desktop/src/lib/sql/layout/internals.ts index f872a1cb22..10336d4d0c 100644 --- a/apps/desktop/src/lib/sql/layout/internals.ts +++ b/apps/desktop/src/lib/sql/layout/internals.ts @@ -112,7 +112,16 @@ async function importEngine(): Promise { /** Loads (and caches) the engine, or `null` when the internals are unavailable. */ export function loadEngine(): Promise { - enginePromise ??= importEngine(); + if (!enginePromise) { + // Vite can briefly fail a first dynamic import while it optimizes the deep + // imports. Do not let one transient miss pin every later Format action to + // sql-formatter's fallback layout: retry once now, then cache only success. + const pending = importEngine().then((engine) => engine ?? importEngine()); + enginePromise = pending; + void pending.then((engine) => { + if (!engine && enginePromise === pending) enginePromise = null; + }); + } return enginePromise; } diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index c4ad7eef0e..c3598892d0 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -79,6 +79,16 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ welcomePageMode: "workspace", welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION }).welcomePageMode).toBe("workspace"); }); + it("defaults SQL formatter layout to DBX and preserves an explicitly selected Classic layout", () => { + expect(DEFAULT_EDITOR_SETTINGS.sqlFormatter.layoutStyle).toBe("dbx"); + expect(normalizeEditorSettings({}).sqlFormatter.layoutStyle).toBe("dbx"); + expect( + normalizeEditorSettings({ + sqlFormatter: { ...DEFAULT_EDITOR_SETTINGS.sqlFormatter, layoutStyle: "classic" }, + }).sqlFormatter.layoutStyle, + ).toBe("classic"); + }); + it("defaults compact tab titles on and keeps an explicit choice packed with its version marker", () => { expect(DEFAULT_EDITOR_SETTINGS.compactTabTitle).toBe(true); // Fresh blobs never saw the old unwrapped default, so they take the marker and the new default. @@ -789,6 +799,11 @@ describe("normalizeEditorSettings", () => { fontFamily: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", }).fontFamily, ).toBe(DEFAULT_MONO_FONT_FAMILY); + expect( + normalizeEditorSettings({ + fontFamily: `'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`, + }).fontFamily, + ).toBe(DEFAULT_MONO_FONT_FAMILY); expect(normalizeEditorSettings({ fontFamily: "'Custom Mono', monospace" }).fontFamily).toBe("'Custom Mono', monospace"); expect(normalizeEditorSettings({ fontFamily: " " }).fontFamily).toBe(DEFAULT_MONO_FONT_FAMILY); }); @@ -2076,6 +2091,24 @@ describe("settingsStore editor settings persistence", () => { setActivePinia(createPinia()); }); + it("preserves a previously selected Classic SQL formatter layout when loading saved settings", async () => { + const loadEditorSettings = vi.fn().mockResolvedValue({ + sqlFormatter: { ...DEFAULT_EDITOR_SETTINGS.sqlFormatter, layoutStyle: "classic" }, + executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION, + welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION, + sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION, + shortcuts: DEFAULT_EDITOR_SETTINGS.shortcuts, + }); + const saveEditorSettings = vi.fn().mockResolvedValue(undefined); + vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const store = useSettingsStore(); + await store.initEditorSettings(); + + expect(store.editorSettings.sqlFormatter.layoutStyle).toBe("classic"); + }); + it("initializes legacy settings before queueing formatter mutations", async () => { const loadEditorSettings = vi.fn().mockResolvedValue({ updateDownloadSource: "atomgit", diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index fe9170320f..e0560d9520 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -5,7 +5,7 @@ import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { aiConfigToItem, generateId, getConfigKey } from "@/lib/ai/aiConfigList"; import { AI_CONVERSATION_FONT_FAMILY_DEFAULT, AI_CONVERSATION_FONT_SIZE_DEFAULT, normalizeAiConversationFontFamily, normalizeAiConversationFontSize } from "@/lib/ai/aiTypography"; -import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts"; +import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, LEGACY_PRESET_FONT_MAP } from "@/lib/app/appFonts"; import { emitAlwaysOnTopToolbarVisibilityChanged } from "@/lib/app/windowAlwaysOnTop"; import { defaultBackgroundImageSettings, normalizeBackgroundImageSettings, type BackgroundImageSettings } from "@/lib/app/appBackgroundImage"; import * as api from "@/lib/backend/api"; @@ -1568,13 +1568,11 @@ function normalizeUiScale(value: unknown): number { return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100)); } -const LEGACY_DEFAULT_MONO_FONT_FAMILY = "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace"; - -function normalizeFontFamily(value: unknown, fallback: string): string { +function normalizeFontFamily(value: unknown, fallback: string, migrateMonoPresets = false): string { if (typeof value !== "string") return fallback; const trimmed = value.trim(); - if (!trimmed || trimmed === LEGACY_DEFAULT_MONO_FONT_FAMILY) return fallback; - return trimmed; + if (!trimmed) return fallback; + return (migrateMonoPresets ? LEGACY_PRESET_FONT_MAP[trimmed] : undefined) ?? trimmed; } function normalizeDrawerWidth(value: unknown, min: number, fallback: number): number { @@ -1975,7 +1973,7 @@ export function normalizeEditorSettings(settings: Partial, exist })(); const activeCustomThemeId = settings.activeCustomThemeId && customThemes.some((t) => t.id === settings.activeCustomThemeId) ? settings.activeCustomThemeId : (customThemes[0]?.id ?? "default"); return { - fontFamily: normalizeFontFamily(settings.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily), + fontFamily: normalizeFontFamily(settings.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily, true), fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize, uiFontFamily: normalizeFontFamily(settings.uiFontFamily, DEFAULT_EDITOR_SETTINGS.uiFontFamily), aiFontFamily: normalizeAiConversationFontFamily(settings.aiFontFamily), diff --git a/apps/desktop/src/styles/tokens.css b/apps/desktop/src/styles/tokens.css index 0e5ea20952..cbcdd7b7d8 100644 --- a/apps/desktop/src/styles/tokens.css +++ b/apps/desktop/src/styles/tokens.css @@ -42,7 +42,7 @@ --font-heading: var(--font-sans); /* Baseline mono token; App.vue overrides it inline with the editor font setting, and plugin sandboxes receive the resolved value via the bridge. */ - --font-mono: 'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace; + --font-mono: 'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei"; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index f0c56d7511..1d9550e65a 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -7,6 +7,8 @@ import { connectionTypesPlugin } from "./viteConnectionTypesPlugin.ts"; const host = process.env.TAURI_DEV_HOST; const isTauri = !!host || !!process.env.TAURI_ENV_ARCH; +const tauriDevPort = configuredPort(process.env.TAURI_DEV_PORT, 1420); +const tauriHmrPort = configuredPort(process.env.TAURI_DEV_HMR_PORT, tauriDevPort + 1); const configuredBasePath = process.env.VITE_DBX_BASE_PATH || process.env.DBX_PUBLIC_BASE_PATH; const manualChunks: Record = { codemirror: ["codemirror", "@codemirror/lang-sql", "@codemirror/view", "@codemirror/state", "@codemirror/autocomplete", "@codemirror/commands", "@codemirror/lint", "@codemirror/theme-one-dark"], @@ -57,6 +59,11 @@ function normalizeViteBase(value: string | undefined): string { return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`; } +function configuredPort(value: string | undefined, fallback: number): number { + const port = Number(value); + return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback; +} + const viteBase = normalizeViteBase(configuredBasePath); const publicBasePath = viteBase.startsWith("/") ? viteBase.replace(/\/+$/, "") : ""; const apiProxyPath = publicBasePath ? `${publicBasePath}/api` : "/api"; @@ -91,14 +98,14 @@ export default defineConfig(async () => ({ }, }, server: { - port: isTauri ? 1420 : undefined, + port: isTauri ? tauriDevPort : undefined, strictPort: isTauri, host: host || false, hmr: host ? { protocol: "ws", host, - port: 1421, + port: tauriHmrPort, } : undefined, proxy: { diff --git a/docs/content/docs/sql-formatter.cn.mdx b/docs/content/docs/sql-formatter.cn.mdx index d243752af2..8984b5c535 100644 --- a/docs/content/docs/sql-formatter.cn.mdx +++ b/docs/content/docs/sql-formatter.cn.mdx @@ -28,9 +28,9 @@ MongoDB 查询编辑器使用单独的 Mongo Shell 格式化器,不使用本 ## 默认风格 -默认的 `dbx` 排版规则:能放下的语句保持一行,放不下的按子句逐行展开,子句的后续元素与第一个元素对齐;条件的逻辑操作符放在续行行首;`CREATE TABLE` 的列定义按列对齐。下面的设置用于调整这些规则,**恢复默认值**会回到该风格。 +默认的 `dbx` 排版采用 DBX 布局:显式 `AS` 别名对齐,`FROM` 与表名同行。短查询仍会保持紧凑,`CREATE TABLE` 定义按列对齐。`classic` 使用 sql-formatter 上游的排版。点击**恢复默认值**会回到当前默认风格。 -把 **排版风格** 设为 `classic` 会改用 sql-formatter 自身的排版,也就是 v0.6.16 之前 DBX 的默认样式。v0.6.16 同时还改过两项默认值,因此还要把 **表达式宽度** 设为 `50`、**FROM 子句布局** 设为换行,才能还原旧输出;等价的 JSON 是 `"expressionWidth": 50`、`"fromClauseLayout": "newLine"`,与 `"layoutStyle": "classic"` 并列。除 Oracle 外结果与 v0.6.15 一致:Oracle 的字节/多字节函数变体(`SUBSTRB`、`INSTR2`、`LENGTHB` 等同族函数)保留较新的修复,会被规范为函数形式。 +`classic` 使用 sql-formatter 自身的排版,也是 v0.6.16 之前 DBX 的默认风格。如需精确还原此前输出,可把**表达式宽度**设为 `50`、**FROM 子句布局**设为换行;JSON 中对应为 `"expressionWidth": 50`、`"fromClauseLayout": "newLine"`,与 `"layoutStyle": "classic"` 并列。除 Oracle 外结果与 v0.6.15 一致:Oracle 的字节/多字节函数变体(`SUBSTRB`、`INSTR2`、`LENGTHB` 等同族函数)保留较新的修复,会被规范为函数形式。 切换排版风格只切引擎,不会顺带改写其它选项。两种缩进样式(`tabularLeft`、`tabularRight`)在两种风格下都使用 sql-formatter 自身的排版。 @@ -40,7 +40,7 @@ MongoDB 查询编辑器使用单独的 Mongo Shell 格式化器,不使用本 | 选项 | 可用值或作用 | | --- | --- | -| 排版风格 | `dbx`(DBX 自研排版,默认)或 `classic`(sql-formatter 上游排版,同 v0.6.15) | +| 排版风格 | `dbx`(DBX 排版,默认)或 `classic`(sql-formatter 上游排版) | | 关键字、数据类型、函数、标识符大小写 | `upper`、`lower`、`preserve` | | 缩进样式 | `standard`、`tabularLeft`、`tabularRight` | | 缩进字符 | 空格或 Tab | diff --git a/docs/content/docs/sql-formatter.mdx b/docs/content/docs/sql-formatter.mdx index 07a25fda7b..eeefa06ea1 100644 --- a/docs/content/docs/sql-formatter.mdx +++ b/docs/content/docs/sql-formatter.mdx @@ -28,9 +28,9 @@ The MongoDB query editor uses a separate Mongo Shell formatter rather than this ## Default Style -The default `dbx` layout keeps a statement that fits the width on one line, puts each clause of a longer one on its own line, and aligns a clause's remaining elements under its first element. Conditions keep their logical operators at the start of the continuation line, and `CREATE TABLE` definitions are aligned on a column grid. The settings below change those rules; **Restore Defaults** returns to this style. +The default `dbx` layout uses DBX's layout rules: explicit `AS` aliases align, and `FROM` stays with its source. Short queries stay compact, and `CREATE TABLE` definitions align on a column grid. `classic` uses sql-formatter's upstream layout. **Restore Defaults** returns to the current default style. -Set **Layout style** to `classic` to use sql-formatter's own layout instead — the default DBX shipped before v0.6.16. v0.6.16 also changed two option defaults, so also set **Expression width** to `50` and **FROM clause layout** to the new-line value to reproduce the old output; the JSON equivalent is `"expressionWidth": 50` and `"fromClauseLayout": "newLine"`, next to `"layoutStyle": "classic"`. The result matches v0.6.15 except on Oracle, where the byte/multibyte function variants (`SUBSTRB`, `INSTR2`, `LENGTHB` and the rest of that family) keep the newer fix that normalizes them as functions. +`classic` uses sql-formatter's own layout and is also the DBX default from before v0.6.16. To reproduce that earlier output exactly, set **Expression width** to `50` and **FROM clause layout** to the new-line value; the JSON equivalent is `"expressionWidth": 50` and `"fromClauseLayout": "newLine"` next to `"layoutStyle": "classic"`. The result matches v0.6.15 except on Oracle, where the byte/multibyte function variants (`SUBSTRB`, `INSTR2`, `LENGTHB` and the rest of that family) keep the newer fix that normalizes them as functions. Switching the layout style only changes the engine; it never rewrites the other options. The two indent styles (`tabularLeft`, `tabularRight`) always use sql-formatter's own layout, in either mode. @@ -40,7 +40,7 @@ Open **Settings → SQL Formatter**. Form mode exposes: | Option | Values or behavior | | --- | --- | -| Layout style | `dbx` (DBX's own layout, default) or `classic` (sql-formatter's upstream layout, as in v0.6.15) | +| Layout style | `dbx` (DBX layout, default) or `classic` (sql-formatter's upstream layout) | | Keyword, data type, function, and identifier case | `upper`, `lower`, or `preserve` | | Indent style | `standard`, `tabularLeft`, or `tabularRight` | | Indentation | Spaces or tabs | diff --git a/packages/app-tests/sqlFormatter.test.ts b/packages/app-tests/sqlFormatter.test.ts index fdd2614dbd..549e1d6139 100644 --- a/packages/app-tests/sqlFormatter.test.ts +++ b/packages/app-tests/sqlFormatter.test.ts @@ -29,14 +29,14 @@ test("formats SQL with uppercase keywords and readable line breaks by default", assert.match(formatted, /\nORDER BY\b/); }); -test("puts each SELECT field on its own line even when the statement fits on one line", async () => { +test("aligns SELECT field continuations under the first field", async () => { const formatted = await formatSqlText("select id, name from users where active = 1 order by name", "postgres"); assert.equal(formatted, "SELECT id,\n name\nFROM users\nWHERE active = 1\nORDER BY name"); assert.equal(await formatSqlText(formatted, "postgres"), formatted); }); -test("aligns explicit SELECT aliases to the widest expression", async () => { +test("aligns explicit SELECT aliases in the default layout", async () => { const formatted = await formatSqlText("select bh.id as unique_value, bh.title as title, bh.house_type as type from biz_house bh", "mysql"); assert.equal( @@ -336,4 +336,3 @@ test("formats SQL preserving original keyword case when keywordCase is preserve assert.doesNotMatch(formatted, /\bFROM\b/); assert.doesNotMatch(formatted, /\bWHERE\b/); }); -