From 4a478acf076fc5df27e68f34dd4b718ef2157c0c Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Mon, 24 Aug 2026 18:35:34 +0800 Subject: [PATCH] feat(editor): allow hiding query line numbers --- .../editor/EditorSettingsDialog.vue | 15 ++++ .../src/components/editor/QueryEditor.vue | 25 +++++-- apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.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/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../__tests__/queryEditorLineNumbers.spec.ts | 74 +++++++++++++++++++ .../src/lib/editor/queryEditorLineNumbers.ts | 9 +++ .../__tests__/editorSettingsDraft.spec.ts | 14 ++++ .../settings/__tests__/settingsSearch.spec.ts | 24 ++++++ .../src/lib/settings/editorSettingsDraft.ts | 1 + .../src/lib/settings/settingsSearch.ts | 1 + .../stores/__tests__/settingsStore.spec.ts | 29 ++++++++ apps/desktop/src/stores/settingsStore.ts | 4 + 18 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/lib/editor/__tests__/queryEditorLineNumbers.spec.ts create mode 100644 apps/desktop/src/lib/editor/queryEditorLineNumbers.ts diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 0b7329aa4f..a84a73346e 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -325,6 +325,7 @@ const executeModeLabel = computed(() => translateWithExecuteShortcut("settings.e const editExecuteAllOnBlankLine = ref(settingsStore.editorSettings.executeAllOnBlankLine); const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker); const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons); +const editShowLineNumbers = ref(settingsStore.editorSettings.showLineNumbers); const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame); const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValueHints); const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables); @@ -526,6 +527,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { executeAllOnBlankLine: editExecuteAllOnBlankLine.value, showExecutionTargetPicker: editShowExecutionTargetPicker.value, showStatementRunButtons: editShowStatementRunButtons.value, + showLineNumbers: editShowLineNumbers.value, showCurrentStatementFrame: editShowCurrentStatementFrame.value, showInsertValueHints: editShowInsertValueHints.value, autoAliasTables: editAutoAliasTables.value, @@ -812,6 +814,7 @@ function syncEditorSettingsDraftFromStore() { editExecuteAllOnBlankLine.value = settingsStore.editorSettings.executeAllOnBlankLine; editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker; editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons; + editShowLineNumbers.value = settingsStore.editorSettings.showLineNumbers; editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame; editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints; editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables; @@ -1063,6 +1066,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; + editShowLineNumbers.value = DEFAULT_EDITOR_SETTINGS.showLineNumbers; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints; editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables; @@ -1175,6 +1179,7 @@ function resetAllDefaults() { editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; + editShowLineNumbers.value = DEFAULT_EDITOR_SETTINGS.showLineNumbers; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints; editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables; @@ -3938,6 +3943,16 @@ onUnmounted(() => { +
+
+ +

+ {{ t("settings.showLineNumbersDescription") }} +

+
+ +
+
diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 4754fdac42..413fb33c17 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -107,6 +107,7 @@ import type { SqlHighlighter } from "@/lib/sql/sqlHighlighter"; import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, loadEditorTheme, editorFontTheme, shellLineCommentTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes"; import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter"; import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap"; +import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers"; import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap"; import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap"; import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion"; @@ -512,12 +513,14 @@ interface EditorGestureEvent extends Event { } let editorViewModule: typeof import("@codemirror/view") | null = null; +let codeMirrorLineNumbers: typeof import("@codemirror/view").lineNumbers | null = null; let codeMirrorPrec: typeof import("@codemirror/state").Prec | null = null; let codeMirrorEditorSelection: typeof import("@codemirror/state").EditorSelection | null = null; let hoverCloseEffect: StateEffect | null = null; let fontThemeComp: import("@codemirror/state").Compartment | null = null; let codeMirrorTheme: import("@codemirror/state").Compartment | null = null; let wordWrapComp: import("@codemirror/state").Compartment | null = null; +let lineNumbersComp: import("@codemirror/state").Compartment | null = null; let vimModeComp: import("@codemirror/state").Compartment | null = null; let closeBracketsComp: import("@codemirror/state").Compartment | null = null; let sqlLanguageComp: import("@codemirror/state").Compartment | null = null; @@ -694,6 +697,7 @@ const queryEditorAppearanceSettings = computed(() => { autoCloseBrackets: settings.autoCloseBrackets, showCurrentStatementFrame: settings.showCurrentStatementFrame, showInsertValueHints: settings.showInsertValueHints, + showLineNumbers: settings.showLineNumbers, shortcuts: settings.shortcuts, showStatementRunButtons: settings.showStatementRunButtons, }; @@ -2053,6 +2057,14 @@ function wordWrapExtension() { return props.forceWordWrap || settingsStore.editorSettings.wordWrap ? editorViewModule.EditorView.lineWrapping : []; } +function lineNumbersExtension(enabled = settingsStore.editorSettings.showLineNumbers) { + return buildQueryEditorLineNumbersExtension(codeMirrorLineNumbers, enabled, { + domEventHandlers: { + mousedown: selectSqlLineFromGutter, + }, + }); +} + function closeBracketsExtension(enabled = settingsStore.editorSettings.autoCloseBrackets) { if (!enabled || !codeMirrorCloseBrackets) return []; const exts: import("@codemirror/state").Extension[] = [codeMirrorCloseBrackets()]; @@ -4649,12 +4661,14 @@ onMounted(async () => { rectangularSelection, } as typeof import("@codemirror/view"); hoverCloseEffect = closeHoverTooltips; + codeMirrorLineNumbers = lineNumbers; codeMirrorPrec = Prec; codeMirrorEditorSelection = EditorSelection; codeMirrorSnippetCompletion = snippetCompletion; fontThemeComp = new Compartment(); codeMirrorTheme = new Compartment(); wordWrapComp = new Compartment(); + lineNumbersComp = new Compartment(); vimModeComp = new Compartment(); closeBracketsComp = new Compartment(); sqlLanguageComp = new Compartment(); @@ -5115,11 +5129,7 @@ onMounted(async () => { scrollToMatch: (range) => EditorView.scrollIntoView(range, { y: "center" }), }), runGutterComp.of(runStatementGutterExtension()), - lineNumbers({ - domEventHandlers: { - mousedown: selectSqlLineFromGutter, - }, - }), + lineNumbersComp.of(lineNumbersExtension(initialSettings.showLineNumbers)), currentStatementFrameExtension, highlightActiveLineGutter(), highlightSpecialChars(), @@ -5763,7 +5773,7 @@ function getCurrentCustomThemeColors() { watch( [queryEditorAppearanceSettings, () => isDark.value, () => themePalette.value, editorThemeAppearance], async ([ss]) => { - if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) { + if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !lineNumbersComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) { return; } if (!isGestureZooming.value && !zoomCommitScheduler.hasPendingCommit() && liveFontSize.value !== ss.fontSize) { @@ -5773,13 +5783,14 @@ watch( syncEditorDiagnosticCssVars(); const themeColors = getCurrentCustomThemeColors(); const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]); - if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) { + if (!view.value || !codeMirrorTheme || !wordWrapComp || !lineNumbersComp || !vimModeComp || !closeBracketsComp || !runGutterComp || !runKeymapComp || !editorViewModule) { return; } view.value.dispatch({ effects: [ codeMirrorTheme.reconfigure(themeExt), wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []), + lineNumbersComp.reconfigure(lineNumbersExtension(ss.showLineNumbers)), vimModeComp.reconfigure(vimModeExtension(settingsStore.editorSettings.vimModeEnabled)), closeBracketsComp.reconfigure(closeBracketsExtension(settingsStore.editorSettings.autoCloseBrackets)), runGutterComp.reconfigure(runStatementGutterExtension()), diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 6c9d26f385..2049385e12 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6369,6 +6369,8 @@ export default { showExecutionTargetPickerDescription: "When enabled, running without a selection lets you choose between the current statement and all SQL.", showStatementRunButtons: "Show left-side run buttons", showStatementRunButtonsDescription: "Show per-statement run buttons in the SQL editor gutter. Keyboard shortcuts and context menu execution still work when disabled.", + showLineNumbers: "Show line numbers", + showLineNumbersDescription: "Show line numbers in the SQL editor gutter", showCurrentStatementFrame: "Show current statement frame", showCurrentStatementFrameDescription: "When enabled, the SQL editor draws an outline around the current executable statement; when disabled, the outline is hidden.", showInsertValueHints: "Show INSERT value column hints", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 8c9fef979a..50f524bb20 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -6055,6 +6055,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "Al activarlo, ejecutar sin selección permite elegir entre la sentencia actual y todo el SQL.", showStatementRunButtons: "Mostrar botones de ejecución laterales", showStatementRunButtonsDescription: "Muestra botones para ejecutar cada sentencia en el margen del editor SQL. Los atajos de teclado y el menú contextual seguirán funcionando al desactivarlo.", + showLineNumbers: "Mostrar números de línea", + showLineNumbersDescription: "Mostrar números de línea en el margen del editor SQL", showCurrentStatementFrame: "Mostrar marco de la sentencia actual", showCurrentStatementFrameDescription: "Al activarlo, el editor SQL dibuja un contorno alrededor de la sentencia ejecutable actual; al desactivarlo, se oculta.", showInsertValueHints: "Mostrar pistas de columnas en VALUES de INSERT", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index c9bc7aee53..39082f5481 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -6055,6 +6055,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "Se attivo, l'esecuzione senza selezione permette di scegliere tra istruzione corrente e tutto l'SQL.", showStatementRunButtons: "Mostra pulsanti di esecuzione laterali", showStatementRunButtonsDescription: "Mostra nel margine dell'editor SQL i pulsanti per eseguire ogni istruzione. Scorciatoie da tastiera e menu contestuale continuano a funzionare quando disattivati.", + showLineNumbers: "Mostra numeri di riga", + showLineNumbersDescription: "Mostra i numeri di riga nel margine dell'editor SQL", showCurrentStatementFrame: "Mostra cornice istruzione corrente", showCurrentStatementFrameDescription: "Se attivo, l'editor SQL disegna un contorno intorno all'istruzione eseguibile corrente; se disattivato, il contorno è nascosto.", showInsertValueHints: "Mostra suggerimenti colonne nei VALUES di INSERT", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 59f1d937a4..832d0d80ab 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -6081,6 +6081,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "有効にすると、選択なしで実行するときに現在の文とすべてのSQLを一時的に選べます。", showStatementRunButtons: "左側の実行ボタンを表示", showStatementRunButtonsDescription: "SQLエディタのガターに文ごとの実行ボタンを表示します。無効にしてもキーボードショートカットとコンテキストメニューからの実行は引き続き使えます。", + showLineNumbers: "行番号を表示", + showLineNumbersDescription: "SQLエディタのガターに行番号を表示します", showCurrentStatementFrame: "現在の文の枠線を表示", showCurrentStatementFrameDescription: "有効にすると、SQLエディタで現在実行可能な文を枠線で示します。無効にすると枠線を表示しません。", showInsertValueHints: "INSERT 値の列名ヒントを表示", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 31f85d5d06..e0cc7e36d6 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5756,6 +5756,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "활성화하면 선택 없이 실행할 때 현재 구문과 전체 SQL 중에서 선택할 수 있습니다.", showStatementRunButtons: "왼쪽 실행 버튼 표시", showStatementRunButtonsDescription: "SQL 편집기 여백에 구문별 실행 버튼을 표시합니다. 비활성화해도 키보드 단축키와 컨텍스트 메뉴 실행은 계속 작동합니다.", + showLineNumbers: "줄 번호 표시", + showLineNumbersDescription: "SQL 편집기 여백에 줄 번호를 표시합니다", showCurrentStatementFrame: "현재 구문 프레임 표시", showCurrentStatementFrameDescription: "활성화하면 SQL 편집기가 현재 실행 가능한 구문 주위에 윤곽선을 그립니다. 비활성화하면 윤곽선이 숨겨집니다.", showInsertValueHints: "INSERT 값 컬럼 힌트 표시", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 03a676672c..a5a08b1c15 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -6057,6 +6057,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "Quando ativado, executar sem seleção permite escolher entre a instrução atual e todo o SQL.", showStatementRunButtons: "Mostrar botões de execução laterais", showStatementRunButtonsDescription: "Mostra botões para executar cada instrução na margem do editor SQL. Atalhos de teclado e execução pelo menu de contexto continuam funcionando quando desativado.", + showLineNumbers: "Mostrar números de linha", + showLineNumbersDescription: "Mostrar números de linha na margem do editor SQL", showCurrentStatementFrame: "Mostrar moldura da instrução atual", showCurrentStatementFrameDescription: "Quando ativado, o editor SQL desenha um contorno ao redor da instrução executável atual; quando desativado, o contorno fica oculto.", showInsertValueHints: "Mostrar dicas de colunas em VALUES de INSERT", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 397e0cc087..b52a8fc808 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6352,6 +6352,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "开启后,无选区执行时可在当前语句和全部 SQL 之间临时选择。", showStatementRunButtons: "显示左侧执行按钮", showStatementRunButtonsDescription: "在 SQL 编辑器左侧显示按语句执行的快捷按钮。关闭后仍可通过快捷键和右键菜单执行。", + showLineNumbers: "显示行号", + showLineNumbersDescription: "在 SQL 编辑器左侧显示行号", showCurrentStatementFrame: "显示当前语句外框线", showCurrentStatementFrameDescription: "开启后,在 SQL 编辑器中用外框线标出当前可执行语句;关闭后不显示外框线。", showInsertValueHints: "显示 INSERT 值列名提示", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 1c43cd0274..8222de52a7 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5373,6 +5373,8 @@ export default withEnglishFallback({ showExecutionTargetPickerDescription: "啟用後,無選取執行時可在目前語句與全部 SQL 之間臨時選擇。", showStatementRunButtons: "顯示左側執行按鈕", showStatementRunButtonsDescription: "在 SQL 編輯器左側顯示按語句執行的快捷按鈕。關閉後仍可透過快捷鍵和右鍵選單執行。", + showLineNumbers: "顯示行號", + showLineNumbersDescription: "在 SQL 編輯器左側顯示行號", showCurrentStatementFrame: "顯示目前語句外框線", showCurrentStatementFrameDescription: "啟用後,在 SQL 編輯器中用外框線標出目前可執行語句;關閉後不顯示外框線。", showInsertValueHints: "顯示 INSERT 值欄位提示", diff --git a/apps/desktop/src/lib/editor/__tests__/queryEditorLineNumbers.spec.ts b/apps/desktop/src/lib/editor/__tests__/queryEditorLineNumbers.spec.ts new file mode 100644 index 0000000000..9e38893b0e --- /dev/null +++ b/apps/desktop/src/lib/editor/__tests__/queryEditorLineNumbers.spec.ts @@ -0,0 +1,74 @@ +// @vitest-environment happy-dom +import { Compartment, EditorState } from "@codemirror/state"; +import { EditorView, GutterMarker, gutter, lineNumbers } from "@codemirror/view"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers"; + +class StatementRunMarker extends GutterMarker {} + +const activeViews: EditorView[] = []; + +function createEditor(showLineNumbers: boolean) { + const lineNumbersCompartment = new Compartment(); + const runGutterCompartment = new Compartment(); + const runMarker = new StatementRunMarker(); + const selectLine = vi.fn((_view: EditorView, _line: { from: number; to: number }, _event: Event) => true); + const view = new EditorView({ + state: EditorState.create({ + doc: "SELECT 1;\nSELECT 2;", + extensions: [ + lineNumbersCompartment.of( + buildQueryEditorLineNumbersExtension(lineNumbers, showLineNumbers, { + domEventHandlers: { mousedown: selectLine }, + }), + ), + runGutterCompartment.of( + gutter({ + class: "cm-run-statement-gutter", + lineMarker: () => runMarker, + }), + ), + ], + }), + parent: document.createElement("div"), + }); + activeViews.push(view); + return { lineNumbersCompartment, selectLine, view }; +} + +afterEach(() => { + for (const view of activeViews.splice(0)) view.destroy(); +}); + +describe("query editor line numbers", () => { + it("keeps line selection behavior when enabled", () => { + const { selectLine, view } = createEditor(true); + const lineNumber = view.dom.querySelector(".cm-lineNumbers .cm-gutterElement"); + + lineNumber?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 })); + + expect(view.dom.querySelector(".cm-lineNumbers")).not.toBeNull(); + expect(selectLine).toHaveBeenCalledOnce(); + }); + + it("reconfigures line numbers without removing the statement run gutter", () => { + const { lineNumbersCompartment, view } = createEditor(true); + + expect(view.dom.querySelector(".cm-lineNumbers")).not.toBeNull(); + expect(view.dom.querySelector(".cm-run-statement-gutter")).not.toBeNull(); + + view.dispatch({ + effects: lineNumbersCompartment.reconfigure(buildQueryEditorLineNumbersExtension(lineNumbers, false, { domEventHandlers: {} })), + }); + + expect(view.dom.querySelector(".cm-lineNumbers")).toBeNull(); + expect(view.dom.querySelector(".cm-run-statement-gutter")).not.toBeNull(); + + view.dispatch({ + effects: lineNumbersCompartment.reconfigure(buildQueryEditorLineNumbersExtension(lineNumbers, true, { domEventHandlers: {} })), + }); + + expect(view.dom.querySelector(".cm-lineNumbers")).not.toBeNull(); + expect(view.dom.querySelector(".cm-run-statement-gutter")).not.toBeNull(); + }); +}); diff --git a/apps/desktop/src/lib/editor/queryEditorLineNumbers.ts b/apps/desktop/src/lib/editor/queryEditorLineNumbers.ts new file mode 100644 index 0000000000..d12b427bf6 --- /dev/null +++ b/apps/desktop/src/lib/editor/queryEditorLineNumbers.ts @@ -0,0 +1,9 @@ +import type { Extension } from "@codemirror/state"; +import type { lineNumbers } from "@codemirror/view"; + +type LineNumbersFactory = typeof lineNumbers; +type LineNumbersConfig = NonNullable[0]>; + +export function buildQueryEditorLineNumbersExtension(factory: LineNumbersFactory | null, enabled: boolean, config: LineNumbersConfig): Extension { + return enabled && factory ? factory(config) : []; +} diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index fadc8c696e..545305807b 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -38,6 +38,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).not.toContain("globalQueryTimeoutSecs"); }); + it("includes showLineNumbers", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("showLineNumbers"); + }); + it("includes continueOnErrorOnBatch", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("continueOnErrorOnBatch"); }); @@ -103,6 +107,16 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { }); describe("editorSettingsDraftFromSettings", () => { + it("round-trips a hidden line-number preference through the draft patch", () => { + const settings = makeSettings({ showLineNumbers: true }); + const draft = editorSettingsDraftFromSettings(settings); + const base = editorSettingsDraftFromSettings(settings); + + draft.showLineNumbers = false; + + expect(editorSettingsPatchFromDraft(draft, base)).toEqual({ showLineNumbers: false }); + }); + it("toggles substitution without discarding per-database overrides", () => { const base = editorSettingsDraftFromSettings( makeSettings({ diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 7839ad5f5c..fde1e25fad 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -37,6 +37,30 @@ describe("settings search", () => { { id: "desktop", category: "about", titleKey: "hidden", visible: ({ isWeb }) => !isWeb }, ]; + it("indexes the query editor line-number preference", () => { + const definition = SETTINGS_SEARCH_DEFINITIONS.find((entry) => entry.id === "editor-line-numbers"); + expect(definition).toEqual({ + id: "editor-line-numbers", + category: "editor", + titleKey: "settings.showLineNumbers", + descriptionKey: "settings.showLineNumbersDescription", + targetId: "editor", + }); + + const entries = resolveSettingsSearchEntries( + [definition!], + { isWeb: false, visibleCategories: new Set(["editor"]) }, + (key) => + ({ + "settings.showLineNumbers": "Show line numbers", + "settings.showLineNumbersDescription": "Show line numbers in the SQL editor gutter", + })[key] ?? key, + categoryLabels, + ); + + expect(searchSettings(entries, "line number", "en").map((entry) => entry.id)).toEqual(["editor-line-numbers"]); + }); + it("does not index connection or query timeout under editor settings", () => { expect(SETTINGS_SEARCH_DEFINITIONS.map((definition) => definition.id)).not.toContain("editor-global-connect-timeout"); expect(SETTINGS_SEARCH_DEFINITIONS.map((definition) => definition.id)).not.toContain("editor-global-query-timeout"); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 470d3f678a..2c31b01863 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -16,6 +16,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "executeAllOnBlankLine", "showExecutionTargetPicker", "showStatementRunButtons", + "showLineNumbers", "showCurrentStatementFrame", "showInsertValueHints", "autoAliasTables", diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index a8b6dcd2ce..b4b46e322b 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -109,6 +109,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-execute-all-on-blank-line", category: "editor", titleKey: "settings.executeAllOnBlankLine", descriptionKey: "settings.executeAllOnBlankLineDescription", targetId: "editor" }, { id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" }, { id: "editor-run-buttons", category: "editor", titleKey: "settings.showStatementRunButtons", descriptionKey: "settings.showStatementRunButtonsDescription", targetId: "editor" }, + { id: "editor-line-numbers", category: "editor", titleKey: "settings.showLineNumbers", descriptionKey: "settings.showLineNumbersDescription", targetId: "editor" }, { id: "editor-statement-frame", category: "editor", titleKey: "settings.showCurrentStatementFrame", descriptionKey: "settings.showCurrentStatementFrameDescription", targetId: "editor" }, { id: "editor-value-hints", category: "editor", titleKey: "settings.showInsertValueHints", descriptionKey: "settings.showInsertValueHintsDescription", targetId: "editor" }, { id: "editor-word-wrap", category: "editor", titleKey: "settings.wordWrap", descriptionKey: "settings.wordWrapDescription", targetId: "editor" }, diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index 51fa279e08..f9688ec771 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -131,6 +131,12 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ sidebarConnectionSortMode: "invalid" as any }).sidebarConnectionSortMode).toBe("manual"); }); + it("shows line numbers by default and preserves an explicit opt-out", () => { + expect(normalizeEditorSettings({}).showLineNumbers).toBe(true); + expect(normalizeEditorSettings({ showLineNumbers: false }).showLineNumbers).toBe(false); + expect(normalizeEditorSettings({ showLineNumbers: "false" } as any).showLineNumbers).toBe(true); + }); + it("shows the current statement frame by default", () => { expect(normalizeEditorSettings({}).showCurrentStatementFrame).toBe(true); }); @@ -726,6 +732,29 @@ describe("settingsStore persisted settings initialization", () => { expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ openDataTabsNextToActive: false })); }); + it("loads, persists, and reloads hidden query editor line numbers", async () => { + let persistedSettings: Record = { showLineNumbers: true }; + const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings))); + const saveEditorSettings = vi.fn(async (settings: Record) => { + persistedSettings = JSON.parse(JSON.stringify(settings)); + }); + vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const store = useSettingsStore(); + await store.initEditorSettings(); + await store.updateEditorSettingsAndPersist({ showLineNumbers: false }); + + expect(store.editorSettings.showLineNumbers).toBe(false); + expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ showLineNumbers: false })); + + setActivePinia(createPinia()); + const restartedStore = useSettingsStore(); + await restartedStore.initEditorSettings(); + + expect(restartedStore.editorSettings.showLineNumbers).toBe(false); + }); + it("shares concurrent initialization and applies startup changes after saved settings load", async () => { let resolveLoad!: (value: unknown) => void; const loadEditorSettings = vi.fn( diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index ab9a975279..b7a770feb4 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -525,6 +525,7 @@ export interface EditorSettings { timeoutInheritanceMigrationVersion: number; showExecutionTargetPicker: boolean; showStatementRunButtons: boolean; + showLineNumbers: boolean; showCurrentStatementFrame: boolean; showInsertValueHints: boolean; autoAliasTables: boolean; @@ -733,6 +734,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { timeoutInheritanceMigrationVersion: 2, showExecutionTargetPicker: false, showStatementRunButtons: true, + showLineNumbers: true, showCurrentStatementFrame: true, showInsertValueHints: true, autoAliasTables: true, @@ -1105,6 +1107,7 @@ export function normalizeEditorSettings(settings: Partial, exist : 0, showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker, showStatementRunButtons: typeof settings.showStatementRunButtons === "boolean" ? settings.showStatementRunButtons : DEFAULT_EDITOR_SETTINGS.showStatementRunButtons, + showLineNumbers: typeof settings.showLineNumbers === "boolean" ? settings.showLineNumbers : DEFAULT_EDITOR_SETTINGS.showLineNumbers, showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame, showInsertValueHints: typeof settings.showInsertValueHints === "boolean" ? settings.showInsertValueHints : DEFAULT_EDITOR_SETTINGS.showInsertValueHints, autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables, @@ -1705,6 +1708,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.timeoutInheritanceMigrationVersion !== undefined) editorSettings.value.timeoutInheritanceMigrationVersion = Math.max(0, Math.floor(partial.timeoutInheritanceMigrationVersion)); if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker; if (partial.showStatementRunButtons !== undefined) editorSettings.value.showStatementRunButtons = partial.showStatementRunButtons === true; + if (partial.showLineNumbers !== undefined) editorSettings.value.showLineNumbers = partial.showLineNumbers === true; if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true; if (partial.showInsertValueHints !== undefined) editorSettings.value.showInsertValueHints = partial.showInsertValueHints === true; if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables;