diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 17881e53e..38f55254f 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -664,6 +664,7 @@ const editWordWrap = ref(settingsStore.editorSettings.wordWrap); const editShowWhitespace = ref(settingsStore.editorSettings.showWhitespace); const editDdlOpenMode = ref(settingsStore.editorSettings.ddlOpenMode); const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled); +const editDoubleClickStringSelectionMode = ref(settingsStore.editorSettings.doubleClickStringSelectionMode); const editAutoCloseBrackets = ref(settingsStore.editorSettings.autoCloseBrackets); const editSqlSemanticDiagnosticsMode = ref(settingsStore.editorSettings.sqlSemanticDiagnosticsMode); const editSqlSemanticDiagnosticsEnabled = ref(settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled); @@ -1024,6 +1025,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { showWhitespace: editShowWhitespace.value, ddlOpenMode: editDdlOpenMode.value, vimModeEnabled: editVimModeEnabled.value, + doubleClickStringSelectionMode: editDoubleClickStringSelectionMode.value, autoCloseBrackets: editAutoCloseBrackets.value, sqlSemanticDiagnosticsMode: editSqlSemanticDiagnosticsMode.value, confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value, @@ -1684,6 +1686,7 @@ function syncEditorSettingsDraftFromStore() { editShowWhitespace.value = settingsStore.editorSettings.showWhitespace; editDdlOpenMode.value = settingsStore.editorSettings.ddlOpenMode; editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled; + editDoubleClickStringSelectionMode.value = settingsStore.editorSettings.doubleClickStringSelectionMode; editAutoCloseBrackets.value = settingsStore.editorSettings.autoCloseBrackets; editSqlSemanticDiagnosticsMode.value = settingsStore.editorSettings.sqlSemanticDiagnosticsMode; editSqlSemanticDiagnosticsEnabled.value = settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled; @@ -1822,6 +1825,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = { showWhitespace: editShowWhitespace, ddlOpenMode: editDdlOpenMode, vimModeEnabled: editVimModeEnabled, + doubleClickStringSelectionMode: editDoubleClickStringSelectionMode, autoCloseBrackets: editAutoCloseBrackets, sqlSemanticDiagnosticsMode: editSqlSemanticDiagnosticsMode, confirmDangerousSqlExecution: editConfirmDangerousSqlExecution, @@ -2312,6 +2316,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editShowWhitespace.value = DEFAULT_EDITOR_SETTINGS.showWhitespace; editDdlOpenMode.value = DEFAULT_EDITOR_SETTINGS.ddlOpenMode; editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled; + editDoubleClickStringSelectionMode.value = DEFAULT_EDITOR_SETTINGS.doubleClickStringSelectionMode; editAutoCloseBrackets.value = DEFAULT_EDITOR_SETTINGS.autoCloseBrackets; editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode; editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled; @@ -2470,6 +2475,7 @@ function resetAllDefaults() { editShowWhitespace.value = DEFAULT_EDITOR_SETTINGS.showWhitespace; editDdlOpenMode.value = DEFAULT_EDITOR_SETTINGS.ddlOpenMode; editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled; + editDoubleClickStringSelectionMode.value = DEFAULT_EDITOR_SETTINGS.doubleClickStringSelectionMode; editAutoCloseBrackets.value = DEFAULT_EDITOR_SETTINGS.autoCloseBrackets; editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode; editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled; @@ -2869,6 +2875,10 @@ function setDdlOpenMode(value: unknown) { if (value === "dialog" || value === "tab") editDdlOpenMode.value = value; } +function setDoubleClickStringSelectionMode(value: unknown) { + if (value === "content" || value === "word") editDoubleClickStringSelectionMode.value = value; +} + function setIconTheme(value: DesktopIconTheme) { editIconTheme.value = value; } @@ -6605,6 +6615,22 @@ onUnmounted(() => { +
+
+ +

{{ t("settings.doubleClickStringSelectionModeDescription") }}

+
+ +
+
diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index e46effcd2..cf854a549 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1909,6 +1909,7 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({ dialect: sqlBehaviorDialect(), language: queryEditorSelectionLanguage(), composing: isEditorComposing(currentView), + selectStringContent: settingsStore.editorSettings.doubleClickStringSelectionMode !== "word", }), ), ), diff --git a/apps/desktop/src/i18n/__tests__/doubleClickStringSelectionParity.spec.ts b/apps/desktop/src/i18n/__tests__/doubleClickStringSelectionParity.spec.ts new file mode 100644 index 000000000..0cff591a2 --- /dev/null +++ b/apps/desktop/src/i18n/__tests__/doubleClickStringSelectionParity.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import az from "../locales/az"; +import en from "../locales/en"; +import es from "../locales/es"; +import it_ from "../locales/it"; +import ja from "../locales/ja"; +import ko from "../locales/ko"; +import ptBR from "../locales/pt-BR"; +import ru from "../locales/ru"; +import tr from "../locales/tr"; +import zhCN from "../locales/zh-CN"; +import zhTW from "../locales/zh-TW"; + +const locales: Array<[string, Record]> = [ + ["az", az], + ["en", en], + ["es", es], + ["it", it_], + ["ja", ja], + ["ko", ko], + ["pt-BR", ptBR], + ["ru", ru], + ["tr", tr], + ["zh-CN", zhCN], + ["zh-TW", zhTW], +]; + +// The editor settings page renders in whatever locale the user runs, so a +// missing key would show a raw key on the "double click inside a string" row. +const DOUBLE_CLICK_STRING_KEYS = ["doubleClickStringSelectionMode", "doubleClickStringSelectionModeDescription", "doubleClickStringSelectionModeContent", "doubleClickStringSelectionModeWord"] as const; + +describe("double-click string selection locale parity", () => { + it.each(locales)("%s exposes the double-click string copy", (_name, locale) => { + const settings = (locale as { settings: Record }).settings; + for (const key of DOUBLE_CLICK_STRING_KEYS) { + expect(settings[key], `${_name}: settings.${key}`).toBeTypeOf("string"); + expect(settings[key] as string, `${_name}: settings.${key}`).not.toHaveLength(0); + } + }); +}); diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 3f697f54e..7eeb86b57 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -7690,6 +7690,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "Cədvəlin DDL-inə baxarkən standart olaraq dialoqun, yoxsa yeni vərəqin açılacağını seçin", ddlOpenModeDialog: "Dialoq (standart)", ddlOpenModeTab: "Yeni vərəq", + doubleClickStringSelectionMode: "Sətir daxilində ikiqat klik", + doubleClickStringSelectionModeDescription: "Sətir literalı daxilində ikiqat klik edildikdə bütün dəyərin, yoxsa yalnız kursorun altındakı sözün seçiləcəyini təyin edir", + doubleClickStringSelectionModeContent: "Bütün dəyəri seç (standart)", + doubleClickStringSelectionModeWord: "Yalnız sözü seç", vimMode: "Vim rejimi", vimModeDescription: "SQL redaktorunda Vim üslublu modal redaktədən istifadə et", autoCloseBrackets: "Mötərizələri avtomatik bağla", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 31974b494..080d672c1 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -8384,6 +8384,10 @@ export default { ddlOpenModeDescription: "Choose whether viewing table DDL opens a dialog or a new tab by default", ddlOpenModeDialog: "Dialog (default)", ddlOpenModeTab: "New tab", + doubleClickStringSelectionMode: "Double-click inside a string", + doubleClickStringSelectionModeDescription: "Choose whether double-clicking inside a string literal selects the whole string value or only the word under the cursor", + doubleClickStringSelectionModeContent: "Select whole value (default)", + doubleClickStringSelectionModeWord: "Select single word", vimMode: "Vim mode", vimModeDescription: "Use Vim-style modal editing in the SQL editor", autoCloseBrackets: "Auto-close brackets", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index dfdfc5453..760cb7b5f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -7702,6 +7702,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "Elige si, al ver el DDL de una tabla, se abre un diálogo o una pestaña nueva de forma predeterminada", ddlOpenModeDialog: "Diálogo (predeterminado)", ddlOpenModeTab: "Nueva pestaña", + doubleClickStringSelectionMode: "Doble clic dentro de una cadena", + doubleClickStringSelectionModeDescription: "Elige si al hacer doble clic dentro de un literal de cadena se selecciona todo el valor o solo la palabra bajo el cursor", + doubleClickStringSelectionModeContent: "Seleccionar todo el valor (predeterminado)", + doubleClickStringSelectionModeWord: "Seleccionar una sola palabra", vimMode: "Modo Vim", vimModeDescription: "Usar edición modal estilo Vim en el editor SQL", autoCloseBrackets: "Cerrar paréntesis automáticamente", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index e2d83a462..ee17a7ab1 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -7627,6 +7627,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "Scegli se la visualizzazione del DDL di una tabella si apre per impostazione predefinita in una finestra di dialogo o in una nuova scheda", ddlOpenModeDialog: "Finestra di dialogo (predefinita)", ddlOpenModeTab: "Nuova scheda", + doubleClickStringSelectionMode: "Doppio clic dentro una stringa", + doubleClickStringSelectionModeDescription: "Scegli se il doppio clic all'interno di un letterale stringa seleziona l'intero valore o solo la parola sotto il cursore", + doubleClickStringSelectionModeContent: "Seleziona l'intero valore (predefinito)", + doubleClickStringSelectionModeWord: "Seleziona solo la parola", vimMode: "Modalita Vim", vimModeDescription: "Usa la modifica modale in stile Vim nell'editor SQL", autoCloseBrackets: "Chiusura automatica parentesi", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 443f84289..7d305b01d 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -7643,6 +7643,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "テーブルのDDLを表示するとき、ダイアログと新しいタブのどちらを既定にするか選択します", ddlOpenModeDialog: "ダイアログ(既定)", ddlOpenModeTab: "新しいタブ", + doubleClickStringSelectionMode: "文字列のダブルクリック", + doubleClickStringSelectionModeDescription: "SQL エディターで文字列リテラルをダブルクリックしたとき、文字列全体を選択するか、カーソル位置の単語だけを選択するかを選びます", + doubleClickStringSelectionModeContent: "文字列全体を選択(既定)", + doubleClickStringSelectionModeWord: "単語だけを選択", vimMode: "Vimモード", vimModeDescription: "SQLエディタでVim形式のモーダル編集を使用します", autoCloseBrackets: "自動括弧補完", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 84f00e548..1943640a6 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -7417,6 +7417,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "테이블 DDL을 볼 때 대화상자와 새 탭 중 기본 열기 방식을 선택합니다", ddlOpenModeDialog: "대화상자 (기본값)", ddlOpenModeTab: "새 탭", + doubleClickStringSelectionMode: "문자열 더블 클릭", + doubleClickStringSelectionModeDescription: "SQL 편집기에서 문자열 리터럴을 더블 클릭했을 때 문자열 전체를 선택할지, 커서 위치의 단어만 선택할지 정합니다", + doubleClickStringSelectionModeContent: "전체 문자열 선택(기본)", + doubleClickStringSelectionModeWord: "단어만 선택", vimMode: "Vim 모드", vimModeDescription: "SQL 편집기에서 Vim 스타일의 모달 편집을 사용합니다", autoCloseBrackets: "괄호 자동 닫기", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 10308c30a..a421a7666 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -7628,6 +7628,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "Escolha se a visualização do DDL de uma tabela abre em um diálogo ou em uma nova aba por padrão", ddlOpenModeDialog: "Diálogo (padrão)", ddlOpenModeTab: "Nova aba", + doubleClickStringSelectionMode: "Duplo clique dentro de uma string", + doubleClickStringSelectionModeDescription: "Escolha se o duplo clique dentro de um literal de string seleciona o valor inteiro ou apenas a palavra sob o cursor", + doubleClickStringSelectionModeContent: "Selecionar o valor inteiro (padrão)", + doubleClickStringSelectionModeWord: "Selecionar apenas a palavra", vimMode: "Modo Vim", vimModeDescription: "Usar edição modal no estilo Vim no editor SQL", autoCloseBrackets: "Fechar parênteses automaticamente", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index 645bb8dce..5b9749956 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -8887,6 +8887,10 @@ export default withEnglishFallback({ vimModeDescription: "Использовать модальное редактирование в стиле Vim в SQL-редакторе", autoCloseBrackets: "Автозакрытие скобок", autoCloseBracketsDescription: "Автоматически вставлять закрывающие скобки и кавычки при вводе открывающих", + doubleClickStringSelectionMode: "Двойной щелчок внутри строки", + doubleClickStringSelectionModeDescription: "Выберите, что выделять двойным щелчком внутри строкового литерала: всё значение целиком или только слово под курсором", + doubleClickStringSelectionModeContent: "Выделять всё значение (по умолчанию)", + doubleClickStringSelectionModeWord: "Выделять только слово", sqlCompletionSection: "Автодополнение SQL", insertSpaceAfterCompletion: "Вставлять пробел после автодополнения", insertSpaceAfterCompletionDescription: "Добавлять пробел после принятия автодополнения ключевого слова, таблицы или столбца, если следующий символ это позволяет", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index dd9d38a63..fac3697d9 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7586,6 +7586,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "Tablo DDL'sini görüntülerken varsayılan olarak iletişim kutusunun mu yoksa yeni sekmenin mi açılacağını seçin", ddlOpenModeDialog: "İletişim kutusu (varsayılan)", ddlOpenModeTab: "Yeni sekme", + doubleClickStringSelectionMode: "Dize içinde çift tıklama", + doubleClickStringSelectionModeDescription: "Bir dize değişmezi içinde çift tıklandığında tüm değerin mi yoksa yalnızca imlecin altındaki sözcüğün mü seçileceğini belirler", + doubleClickStringSelectionModeContent: "Tüm değeri seç (varsayılan)", + doubleClickStringSelectionModeWord: "Yalnızca sözcüğü seç", vimMode: "Vim modu", vimModeDescription: "SQL düzenleyicisinde Vim biçemli kipli düzenleme kullan", autoCloseBrackets: "Parantezleri otomatik kapat", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index a92da4897..74649e234 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -8347,6 +8347,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "设置查看所有表 DDL 时默认使用弹框还是新标签", ddlOpenModeDialog: "弹框(默认)", ddlOpenModeTab: "新标签", + doubleClickStringSelectionMode: "双击字符串时选中", + doubleClickStringSelectionModeDescription: "在 SQL 编辑器中双击字符串时,选择整段字符串内容,还是只选择双击处的单词", + doubleClickStringSelectionModeContent: "选中整段内容(默认)", + doubleClickStringSelectionModeWord: "只选中单词", vimMode: "Vim 模式", vimModeDescription: "在 SQL 编辑器中使用 Vim 风格的模态编辑", autoCloseBrackets: "自动成对补全", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index d16665c4f..464f0be91 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -6943,6 +6943,10 @@ export default withEnglishFallback({ ddlOpenModeDescription: "設定檢視資料表 DDL 時,預設以對話框或新標籤開啟", ddlOpenModeDialog: "對話框(預設)", ddlOpenModeTab: "新標籤", + doubleClickStringSelectionMode: "雙擊字串時選取", + doubleClickStringSelectionModeDescription: "在 SQL 編輯器中雙擊字串時,要選取整段字串內容,還是只選取雙擊處的單詞", + doubleClickStringSelectionModeContent: "選取整段內容(預設)", + doubleClickStringSelectionModeWord: "只選取單詞", vimMode: "Vim 模式", vimModeDescription: "在 SQL 編輯器中使用 Vim 風格的模態編輯", autoCloseBrackets: "自動成對補全", diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorStringMouseSelection.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorStringMouseSelection.spec.ts index 514f008b0..5eda2c5bf 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorStringMouseSelection.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorStringMouseSelection.spec.ts @@ -162,6 +162,20 @@ describe("query editor SQL string mouse selection", () => { expect(selection && doc.slice(selection.main.from, selection.main.to)).toBe("%%"); }); + it("returns null when the caller opted into plain word selection", () => { + const doc = "EXEC sp_executesql N'SELECT * FROM my_table WHERE a = 1';"; + const state = EditorState.create({ doc }); + const view = fakeView(state).view; + const insideString = eventAt(doc.indexOf("my_table")); + const outsideString = eventAt(doc.indexOf("sp_executesql")); + + expect(createQueryEditorStringMouseSelection(view, insideString)).not.toBeNull(); + expect(createQueryEditorStringMouseSelection(view, insideString, { selectStringContent: false })).toBeNull(); + expect(createQueryEditorStringMouseSelection(view, outsideString, { selectStringContent: false })).toBeNull(); + // Explicitly asking for the historical behaviour keeps working. + expect(createQueryEditorStringMouseSelection(view, insideString, { selectStringContent: true })).not.toBeNull(); + }); + it("trimSqlStringWildcardBoundaries shrinks past boundary wildcards only", () => { const doc = "%a_b%"; expect(trimSqlStringWildcardBoundaries(doc, { from: 0, to: doc.length })).toEqual({ from: 1, to: 4 }); diff --git a/apps/desktop/src/lib/editor/queryEditorStringMouseSelection.ts b/apps/desktop/src/lib/editor/queryEditorStringMouseSelection.ts index 799b7d353..e480d03cb 100644 --- a/apps/desktop/src/lib/editor/queryEditorStringMouseSelection.ts +++ b/apps/desktop/src/lib/editor/queryEditorStringMouseSelection.ts @@ -5,6 +5,12 @@ import { analyzeSqlSemanticSelectionRanges, sqlStringContentRangeAt, trimSqlStri export interface QueryEditorStringMouseSelectionOptions extends SqlSemanticSelectionOptions { language?: "sql" | "text"; composing?: boolean; + /** + * When false the double click keeps the platform default "select one word" + * behaviour instead of selecting the whole string literal. Defaults to true + * so callers that do not opt in keep the historical behaviour. + */ + selectStringContent?: boolean; } interface PointerPosition { @@ -47,6 +53,7 @@ function semanticOrDefaultRange(state: EditorState, position: PointerPosition, o } export function createQueryEditorStringMouseSelection(view: EditorView, event: MouseEvent, options: QueryEditorStringMouseSelectionOptions = {}): MouseSelectionStyle | null { + if (options.selectStringContent === false) return null; if (event.button !== 0 || event.detail !== 2 || event.altKey || options.composing || options.language === "text") return null; let start = view.posAndSideAtCoords({ x: event.clientX, y: event.clientY }, false); let analysis = analyzeSqlSemanticSelectionRanges(view.state.doc.toString(), options); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index ce991b14f..59147ddcf 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -35,6 +35,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "showWhitespace", "ddlOpenMode", "vimModeEnabled", + "doubleClickStringSelectionMode", "autoCloseBrackets", "sqlSemanticDiagnosticsMode", "confirmDangerousSqlExecution", diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 69d1eebe6..3ce05111a 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -142,6 +142,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-word-wrap", category: "editor", titleKey: "settings.wordWrap", descriptionKey: "settings.wordWrapDescription", targetId: "editor" }, { id: "editor-vim", category: "editor", titleKey: "settings.vimMode", descriptionKey: "settings.vimModeDescription", targetId: "editor" }, { id: "editor-brackets", category: "editor", titleKey: "settings.autoCloseBrackets", descriptionKey: "settings.autoCloseBracketsDescription", targetId: "editor" }, + { id: "editor-double-click-string", category: "editor", titleKey: "settings.doubleClickStringSelectionMode", descriptionKey: "settings.doubleClickStringSelectionModeDescription", targetId: "editor" }, { id: "editor-completion-spacing", category: "editor", titleKey: "settings.insertSpaceAfterCompletion", descriptionKey: "settings.insertSpaceAfterCompletionDescription", targetId: "editor" }, { id: "editor-sqlserver-space-completion", category: "editor", titleKey: "settings.sqlServerSpaceConfirmsCompletion", descriptionKey: "settings.sqlServerSpaceConfirmsCompletionDescription", targetId: "editor", visible: sqlServerSpaceCompletionVisible }, { id: "editor-completion-trigger-mode", category: "editor", titleKey: "settings.completionTriggerMode", descriptionKey: "settings.completionTriggerModeDescription", targetId: "editor" }, diff --git a/apps/desktop/src/lib/settings/settingsTransfer.ts b/apps/desktop/src/lib/settings/settingsTransfer.ts index f8e1ee592..c2eb69446 100644 --- a/apps/desktop/src/lib/settings/settingsTransfer.ts +++ b/apps/desktop/src/lib/settings/settingsTransfer.ts @@ -92,6 +92,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record { expect(normalizeEditorSettings({ dataGridSearchMode: "invalid" as any }).dataGridSearchMode).toBe("filter"); }); + it("defaults double-click inside a string to selecting the whole value and preserves word mode", () => { + expect(normalizeEditorSettings({}).doubleClickStringSelectionMode).toBe("content"); + expect(normalizeEditorSettings({ doubleClickStringSelectionMode: "word" }).doubleClickStringSelectionMode).toBe("word"); + expect(normalizeEditorSettings({ doubleClickStringSelectionMode: "invalid" as any }).doubleClickStringSelectionMode).toBe("content"); + }); + it("defaults the data grid row number column to the view position and preserves original row numbers", () => { expect(normalizeEditorSettings({}).dataGridRowNumberMode).toBe("view"); expect(normalizeEditorSettings({ dataGridRowNumberMode: "source" }).dataGridRowNumberMode).toBe("source"); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 2b179f673..88d4e08d7 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -718,6 +718,9 @@ const DATA_GRID_SEARCH_MODES = ["filter", "highlight"] as const; export type DataGridSearchMode = (typeof DATA_GRID_SEARCH_MODES)[number]; const DATA_GRID_ROW_NUMBER_MODES = ["view", "source"] as const; export type DataGridRowNumberMode = (typeof DATA_GRID_ROW_NUMBER_MODES)[number]; +/** How a double click inside a SQL string literal picks text: the whole string value, or just one word. */ +export const DOUBLE_CLICK_STRING_SELECTION_MODES = ["content", "word"] as const; +export type DoubleClickStringSelectionMode = (typeof DOUBLE_CLICK_STRING_SELECTION_MODES)[number]; export type DataGridFilterEditorView = "quick" | "conditions" | "text"; export type DataGridToolbarLayout = "single" | "split"; const RESULT_RUN_DISPLAY_MODES = ["tabs", "list"] as const; @@ -864,6 +867,8 @@ export interface EditorSettings { refreshDdlOnOpen: boolean; excludeDdlStorage: boolean; vimModeEnabled: boolean; + /** Double click inside a string literal selects the whole value ("content") or a single word ("word"). */ + doubleClickStringSelectionMode: DoubleClickStringSelectionMode; autoCloseBrackets: boolean; sqlSemanticDiagnosticsMode: SqlSemanticDiagnosticsMode; sqlSemanticDiagnosticsEnabled: boolean; @@ -1151,6 +1156,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { refreshDdlOnOpen: false, excludeDdlStorage: true, vimModeEnabled: false, + doubleClickStringSelectionMode: "content", autoCloseBrackets: true, sqlSemanticDiagnosticsMode: "auto", sqlSemanticDiagnosticsEnabled: SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED, @@ -1383,6 +1389,10 @@ function normalizeDataGridRowNumberMode(value: unknown): DataGridRowNumberMode { return DATA_GRID_ROW_NUMBER_MODES.includes(value as DataGridRowNumberMode) ? (value as DataGridRowNumberMode) : DEFAULT_EDITOR_SETTINGS.dataGridRowNumberMode; } +function normalizeDoubleClickStringSelectionMode(value: unknown): DoubleClickStringSelectionMode { + return DOUBLE_CLICK_STRING_SELECTION_MODES.includes(value as DoubleClickStringSelectionMode) ? (value as DoubleClickStringSelectionMode) : DEFAULT_EDITOR_SETTINGS.doubleClickStringSelectionMode; +} + function normalizeDataGridFilterEditorView(value: unknown): DataGridFilterEditorView { return value === "conditions" || value === "text" ? value : DEFAULT_EDITOR_SETTINGS.dataGridFilterEditorView; } @@ -1716,6 +1726,7 @@ export function normalizeEditorSettings(settings: Partial, exist ddlOpenMode: settings.ddlOpenMode === "tab" ? "tab" : DEFAULT_EDITOR_SETTINGS.ddlOpenMode, refreshDdlOnOpen: typeof settings.refreshDdlOnOpen === "boolean" ? settings.refreshDdlOnOpen : DEFAULT_EDITOR_SETTINGS.refreshDdlOnOpen, vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled, + doubleClickStringSelectionMode: normalizeDoubleClickStringSelectionMode(settings.doubleClickStringSelectionMode), autoCloseBrackets: typeof settings.autoCloseBrackets === "boolean" ? settings.autoCloseBrackets : DEFAULT_EDITOR_SETTINGS.autoCloseBrackets, sqlSemanticDiagnosticsMode, sqlSemanticDiagnosticsEnabled: sqlSemanticDiagnosticsEnabledForMode(sqlSemanticDiagnosticsMode), @@ -2548,6 +2559,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.ddlOpenMode !== undefined) editorSettings.value.ddlOpenMode = partial.ddlOpenMode === "tab" ? "tab" : "dialog"; if (partial.refreshDdlOnOpen !== undefined) editorSettings.value.refreshDdlOnOpen = partial.refreshDdlOnOpen === true; if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true; + if (partial.doubleClickStringSelectionMode !== undefined) editorSettings.value.doubleClickStringSelectionMode = normalizeDoubleClickStringSelectionMode(partial.doubleClickStringSelectionMode); if (partial.autoCloseBrackets !== undefined) editorSettings.value.autoCloseBrackets = partial.autoCloseBrackets === true; if (partial.sqlSemanticDiagnosticsMode !== undefined || partial.sqlSemanticDiagnosticsEnabled !== undefined) { const nextMode = normalizeSqlSemanticDiagnosticsMode(partial.sqlSemanticDiagnosticsMode, partial.sqlSemanticDiagnosticsEnabled);