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;