From f47c209b152c4eedafadb2dab20d19b5e4ee6e28 Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Sun, 4 Oct 2026 10:02:06 +0800 Subject: [PATCH] feat(editor): add toggle case shortcut for selection --- .../editor/EditorSettingsDialog.vue | 1 + .../src/components/editor/QueryEditor.vue | 1 + .../editor/QueryEditorContextMenu.vue | 11 ++++- 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/tr.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../__tests__/editor/shortcutRegistry.spec.ts | 9 ++++ .../__tests__/sql/sqlSelectionCase.spec.ts | 47 +++++++++++++++++++ .../src/lib/editor/shortcutRegistry.ts | 7 +++ apps/desktop/src/lib/sql/sqlSelectionCase.ts | 12 ++++- docs/content/docs/keyboard-shortcuts.cn.mdx | 1 + docs/content/docs/keyboard-shortcuts.mdx | 1 + 21 files changed, 111 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 5f33255133..300c792bf5 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -2124,6 +2124,7 @@ const formatterEditorShortcutIds: ShortcutActionId[] = [ "selectAll", "uppercaseSelection", "lowercaseSelection", + "toggleCaseSelection", "toggleFold", "foldAll", "unfoldAll", diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index be186d33ac..0d7b0ee7fe 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1396,6 +1396,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") ...binding(shortcuts.selectAllSelectionOccurrences, selectAllQueryEditorSelectionOccurrences), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")), + ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.toggleCaseSelection, () => convertSelectedSqlCase("toggle")), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.convertNamingStyle, () => convertSelectedNamingStyle()), ...binding(shortcuts.toggleLineComment, (view) => codeMirrorRuntime.codeMirrorToggleLineComment?.(view) ?? false), ...binding(shortcuts.toggleBlockComment, (view) => { diff --git a/apps/desktop/src/components/editor/QueryEditorContextMenu.vue b/apps/desktop/src/components/editor/QueryEditorContextMenu.vue index 277d0f8a2f..5f19116996 100644 --- a/apps/desktop/src/components/editor/QueryEditorContextMenu.vue +++ b/apps/desktop/src/components/editor/QueryEditorContextMenu.vue @@ -3,6 +3,7 @@ import { computed } from "vue"; import { useI18n } from "vue-i18n"; import { AlignLeft, + ArrowDownUp, Camera, CaseLower, CaseSensitive, @@ -39,6 +40,7 @@ import { supportsQueryEditorBlockComments } from "@/lib/database/databaseFeature import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import { queryContextObjectActions, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget"; import type { SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation"; +import type { SqlSelectionCaseMode } from "@/lib/sql/sqlSelectionCase"; import type { QueryEditorProps } from "./queryEditorTypes"; export interface QueryEditorContextMenuState { @@ -70,7 +72,7 @@ export interface QueryEditorContextMenuActions { cutSelectedSqlFromContextMenu: () => void; pasteClipboardSqlFromContextMenu: () => void; pasteClipboardSqlRestoringSource: () => void; - convertSelectedSqlCase: (mode: "upper" | "lower") => void; + convertSelectedSqlCase: (mode: SqlSelectionCaseMode) => void; convertSelectedNamingStyle: () => void; openDelimitedListDialog: () => void; addNextSelectionOccurrenceFromContextMenu: () => void; @@ -300,6 +302,13 @@ const contextMenuItems = computed(() => { icon: Sparkles, shortcut: shortcuts.sendSelectionToAi, }, + { + label: t("editor.contextMenu.toggleCaseSelection"), + action: () => actions.convertSelectedSqlCase("toggle"), + disabled: !canCopySelectedSql, + icon: ArrowDownUp, + shortcut: shortcuts.toggleCaseSelection, + }, { label: t("editor.contextMenu.uppercaseSelection"), action: () => actions.convertSelectedSqlCase("upper"), diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index df7ce487b3..9f56dc2d9e 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -1505,6 +1505,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "Yapışdır və mənbə SQL-i bərpa et", pasteClipboardReadFailed: "Mübadilə buferini oxumaq mümkün olmadı: {message}", sendToAi: "Süni intellektə göndər", + toggleCaseSelection: "Registri dəyiş", uppercaseSelection: "Böyük hərflərə çevir", lowercaseSelection: "Kiçik hərflərə çevir", convertNamingStyle: "Adlandırma üslubunu dəyiş", @@ -8222,6 +8223,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Çıxarılanlar", shortcutUppercaseSelection: "Seçimi böyük hərflərə çevir", shortcutLowercaseSelection: "Seçimi kiçik hərflərə çevir", + shortcutToggleCaseSelection: "Seçimin hərf registrini dəyiş", shortcutConvertNamingStyle: "Adlandırma üslubunu dəyiş", shortcutExPasteSqlInCondition: "ExPaste: IN şərti kimi yerləşdir", }, diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index a3839caf5c..cc933aac67 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1802,6 +1802,7 @@ export default { pasteRestoringSourceSql: "Paste and restore source SQL", pasteClipboardReadFailed: "Failed to read clipboard: {message}", sendToAi: "Send to AI", + toggleCaseSelection: "Toggle case", uppercaseSelection: "Convert to uppercase", lowercaseSelection: "Convert to lowercase", convertNamingStyle: "Toggle naming style", @@ -9163,6 +9164,7 @@ export default { changelogSectionRemoved: "Removed", shortcutUppercaseSelection: "Convert selection to uppercase", shortcutLowercaseSelection: "Convert selection to lowercase", + shortcutToggleCaseSelection: "Toggle selection case", shortcutConvertNamingStyle: "Toggle naming style", shortcutExPasteSqlInCondition: "ExPaste: paste as IN condition", }, diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index eb51a66b53..943c083ad2 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1787,6 +1787,7 @@ export default withEnglishFallback({ foldAll: "Contraer todo", unfoldAll: "Expandir todo", sendToAi: "Enviar a IA", + toggleCaseSelection: "Alternar mayúsculas/minúsculas", uppercaseSelection: "Convertir a mayusculas", lowercaseSelection: "Convertir a minusculas", selectAll: "Seleccionar todo", @@ -8460,6 +8461,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Eliminado", shortcutUppercaseSelection: "Convertir selección a mayúsculas", shortcutLowercaseSelection: "Convertir selección a minúsculas", + shortcutToggleCaseSelection: "Alternar mayúsculas y minúsculas de la selección", shortcutExPasteSqlInCondition: "ExPaste: pegar como condición IN", dateTimeSection: "Fecha y hora", globalDateTimeDisplayFormat: "Formato de visualización global", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index c91042a6b9..6e30673ff9 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -1740,6 +1740,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "Tempel dan pulihkan SQL sumber", pasteClipboardReadFailed: "Gagal membaca clipboard: {message}", sendToAi: "Kirim ke AI", + toggleCaseSelection: "Alihkan huruf besar/kecil", uppercaseSelection: "Konversi ke huruf besar", lowercaseSelection: "Konversi ke huruf kecil", convertNamingStyle: "Alihkan gaya penamaan", @@ -8956,6 +8957,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Dihapus", shortcutUppercaseSelection: "Ubah seleksi menjadi huruf besar", shortcutLowercaseSelection: "Ubah seleksi menjadi huruf kecil", + shortcutToggleCaseSelection: "Alihkan huruf besar/kecil seleksi", shortcutConvertNamingStyle: "Alihkan gaya penamaan", shortcutExPasteSqlInCondition: "ExPaste: tempel sebagai kondisi IN", }, diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 56660d6542..724fb68157 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1696,6 +1696,7 @@ export default withEnglishFallback({ foldAll: "Comprimi tutto", unfoldAll: "Espandi tutto", sendToAi: "Invia ad AI", + toggleCaseSelection: "Inverti maiuscole/minuscole", uppercaseSelection: "Converti in maiuscolo", lowercaseSelection: "Converti in minuscolo", selectAll: "Seleziona tutto", @@ -8206,6 +8207,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Rimossi", shortcutUppercaseSelection: "Converti selezione in maiuscolo", shortcutLowercaseSelection: "Converti selezione in minuscolo", + shortcutToggleCaseSelection: "Inverti maiuscole/minuscole della selezione", shortcutExPasteSqlInCondition: "ExPaste: incolla come condizione IN", dateTimeSection: "Data e Ora", globalDateTimeDisplayFormat: "Formato di visualizzazione globale", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 235c68d873..5c1f4beb81 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1716,6 +1716,7 @@ export default withEnglishFallback({ foldAll: "すべて折りたたむ", unfoldAll: "すべて展開", sendToAi: "AIに送信", + toggleCaseSelection: "大文字/小文字を切り替え", uppercaseSelection: "大文字に変換", lowercaseSelection: "小文字に変換", selectAll: "すべて選択", @@ -8222,6 +8223,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "削除", shortcutUppercaseSelection: "選択範囲を大文字に変換", shortcutLowercaseSelection: "選択範囲を小文字に変換", + shortcutToggleCaseSelection: "選択範囲の大文字/小文字を切り替え", shortcutExPasteSqlInCondition: "ExPaste: IN条件として貼り付け", dateTimeSection: "日付と時刻", globalDateTimeDisplayFormat: "グローバル表示形式", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index d550a6878d..e562b9c71d 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1627,6 +1627,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "붙여넣고 소스 SQL 복원", pasteClipboardReadFailed: "클립보드 읽기 실패: {message}", sendToAi: "AI로 보내기", + toggleCaseSelection: "대소문자 전환", uppercaseSelection: "대문자로 변환", lowercaseSelection: "소문자로 변환", delimitedList: "구분자 목록으로 변환", @@ -8012,6 +8013,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "제거된 항목", shortcutUppercaseSelection: "선택 영역을 대문자로 변환", shortcutLowercaseSelection: "선택 영역을 소문자로 변환", + shortcutToggleCaseSelection: "선택 영역 대소문자 전환", shortcutExPasteSqlInCondition: "ExPaste: IN 조건으로 붙여넣기", shortcutToggleFold: "접기/펼치기", shortcutFoldAll: "모두 접기", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index fd98a8bd23..8fdfda7e14 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1697,6 +1697,7 @@ export default withEnglishFallback({ foldAll: "Recolher tudo", unfoldAll: "Expandir tudo", sendToAi: "Enviar para IA", + toggleCaseSelection: "Alternar maiúsculas/minúsculas", uppercaseSelection: "Converter para maiúsculas", lowercaseSelection: "Converter para minúsculas", selectAll: "Selecionar tudo", @@ -8207,6 +8208,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Removido", shortcutUppercaseSelection: "Converter seleção em maiúsculas", shortcutLowercaseSelection: "Converter seleção em minúsculas", + shortcutToggleCaseSelection: "Alternar maiúsculas/minúsculas da seleção", shortcutExPasteSqlInCondition: "ExPaste: colar como condição IN", dateTimeSection: "Data e Hora", globalDateTimeDisplayFormat: "Formato de exibição global", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index 9b3b31d649..a91dd021ae 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -1710,6 +1710,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "Вставить и восстановить SQL из кода", pasteClipboardReadFailed: "Не удалось прочитать буфер обмена: {message}", sendToAi: "Отправить в AI", + toggleCaseSelection: "Переключить регистр", uppercaseSelection: "Преобразовать в верхний регистр", lowercaseSelection: "Преобразовать в нижний регистр", convertNamingStyle: "Переключить стиль именования", @@ -9465,6 +9466,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Удалено", shortcutUppercaseSelection: "Преобразовать выделенное в верхний регистр", shortcutLowercaseSelection: "Преобразовать выделенное в нижний регистр", + shortcutToggleCaseSelection: "Переключить регистр выделенного", shortcutConvertNamingStyle: "Переключить стиль именования", shortcutExPasteSqlInCondition: "ExPaste: вставить как условие IN", showColumnHeaderTooltips: "Всплывающие подсказки при наведении на имена полей", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index c4fb1f9449..647ac0bcd0 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -1505,6 +1505,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "Yapıştır ve kaynak SQL'i geri yükle", pasteClipboardReadFailed: "Pano okunamadı: {message}", sendToAi: "Yapay Zekâya Gönder", + toggleCaseSelection: "Büyük/küçük harf değiştir", uppercaseSelection: "Büyük harfe dönüştür", lowercaseSelection: "Küçük harfe dönüştür", delimitedList: "Ayırıcılı listeye dönüştür", @@ -8096,6 +8097,7 @@ export default withEnglishFallback({ changelogSectionRemoved: "Kaldırıldı", shortcutUppercaseSelection: "Seçimi büyük harfe dönüştür", shortcutLowercaseSelection: "Seçimi küçük harfe dönüştür", + shortcutToggleCaseSelection: "Seçimin büyük/küçük harf durumunu değiştir", shortcutExPasteSqlInCondition: "ExPaste: IN koşulu olarak yapıştır", }, driverStore: { diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 6c73b134c9..b008cd4065 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1738,6 +1738,7 @@ export default withEnglishFallback({ pasteRestoringSourceSql: "粘贴并还原源码 SQL", pasteClipboardReadFailed: "读取剪贴板失败:{message}", sendToAi: "发送到 AI", + toggleCaseSelection: "切换大小写", uppercaseSelection: "转为大写", lowercaseSelection: "转为小写", convertNamingStyle: "切换命名风格", @@ -8624,6 +8625,7 @@ export default withEnglishFallback({ shortcutExtendSelection: "扩展选择", shortcutUppercaseSelection: "选中内容转为大写", shortcutLowercaseSelection: "选中内容转为小写", + shortcutToggleCaseSelection: "切换选中内容大小写", shortcutConvertNamingStyle: "切换命名风格", shortcutExPasteSqlInCondition: "ExPaste:粘贴为 IN 条件", shortcutEditTableStructure: "编辑表结构", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 08b43be53e..24db140171 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1700,6 +1700,7 @@ export default withEnglishFallback({ foldAll: "全部折疊", unfoldAll: "全部展開", sendToAi: "傳送至 AI", + toggleCaseSelection: "切換大小寫", uppercaseSelection: "轉為大寫", lowercaseSelection: "轉為小寫", selectAll: "全選", @@ -7573,6 +7574,7 @@ export default withEnglishFallback({ queryExportKeysetOptimizationEnabledDescription: "僅適用於可安全識別的單表查詢;複雜查詢會自動回退。", shortcutUppercaseSelection: "選取內容轉為大寫", shortcutLowercaseSelection: "選取內容轉為小寫", + shortcutToggleCaseSelection: "切換選取內容大小寫", shortcutExPasteSqlInCondition: "ExPaste:貼上為 IN 條件", routineSourceOpenMode: "函式/程序/檢視開啟方式", routineSourceOpenModeQueryTab: "資料標籤頁", diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index 2f0adec090..d9f01e7f10 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -41,6 +41,7 @@ describe("shortcutRegistry editor actions", () => { "selectAllSelectionOccurrences", "uppercaseSelection", "lowercaseSelection", + "toggleCaseSelection", "exPasteSqlInCondition", "toggleFold", "foldAll", @@ -292,6 +293,7 @@ describe("shortcutRegistry editor actions", () => { expect(shortcuts.selectAllSelectionOccurrences).toBe(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences")); expect(shortcuts.uppercaseSelection).toBe("Shift+Alt+U"); expect(shortcuts.lowercaseSelection).toBe("Shift+Alt+L"); + expect(shortcuts.toggleCaseSelection).toBe("Mod+Shift+U"); expect(shortcuts.exPasteSqlInCondition).toBe(""); expect(shortcuts.toggleFold).toBe("Mod+."); expect(shortcuts.foldAll).toBe(foldAllDefaultShortcut("foldAll")); @@ -308,6 +310,13 @@ describe("shortcutRegistry editor actions", () => { expect(normalizeShortcutSettings({ foldAll: "", unfoldAll: "" }, "Win32")).toMatchObject({ foldAll: "", unfoldAll: "" }); }); + it("registers toggle case as a configurable editor shortcut (#5085)", () => { + const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "toggleCaseSelection"); + + expect(definition).toMatchObject({ scope: "editor", defaultShortcut: "Mod+Shift+U", labelKey: "settings.shortcutToggleCaseSelection" }); + expect(DEFAULT_SHORTCUT_SETTINGS.toggleCaseSelection).toBe("Mod+Shift+U"); + }); + it("registers IntelliJ-style extend selection as a configurable editor shortcut", () => { const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "extendSelection"); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlSelectionCase.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlSelectionCase.spec.ts index 78a7539d82..df888184b1 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlSelectionCase.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlSelectionCase.spec.ts @@ -98,4 +98,51 @@ describe("convertSqlSelectionCase", () => { expect(convertSqlSelectionCase(sql, { from, to: sql.length }, "upper")).toBe("> 60 AND GRADE = 'AbC'"); }); + + describe("toggle mode (#5085)", () => { + it("converts lowercase text to uppercase", () => { + const sql = "select id, name from users"; + expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "toggle")).toBe("SELECT ID, NAME FROM USERS"); + }); + + it("defaults mixed case to uppercase first", () => { + const sql = "Select id, userName From Users"; + expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "toggle")).toBe("SELECT ID, USERNAME FROM USERS"); + }); + + it("converts all-uppercase text to lowercase", () => { + const sql = "SELECT ID, NAME FROM USERS"; + expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "toggle")).toBe("select id, name from users"); + }); + + it("cycles between uppercase and lowercase on repeated toggles", () => { + const initial = "Select Name From Users"; + const first = convertSqlSelectionCase(initial, { from: 0, to: initial.length }, "toggle"); + expect(first).toBe("SELECT NAME FROM USERS"); + + const second = convertSqlSelectionCase(first, { from: 0, to: first.length }, "toggle"); + expect(second).toBe("select name from users"); + + const third = convertSqlSelectionCase(second, { from: 0, to: second.length }, "toggle"); + expect(third).toBe("SELECT NAME FROM USERS"); + }); + + it("protects string literals when toggling case", () => { + const sql = "select name from users where status = 'active'"; + const upper = convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "toggle"); + expect(upper).toBe("SELECT NAME FROM USERS WHERE STATUS = 'active'"); + + const lower = convertSqlSelectionCase(upper, { from: 0, to: upper.length }, "toggle"); + expect(lower).toBe("select name from users where status = 'active'"); + }); + + it("toggles bare string literals when selected directly", () => { + const sql = "('first', 'second')"; + const upper = convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "toggle"); + expect(upper).toBe("('FIRST', 'SECOND')"); + + const lower = convertSqlSelectionCase(upper, { from: 0, to: upper.length }, "toggle"); + expect(lower).toBe("('first', 'second')"); + }); + }); }); diff --git a/apps/desktop/src/lib/editor/shortcutRegistry.ts b/apps/desktop/src/lib/editor/shortcutRegistry.ts index d4f43563af..c6b80055f9 100644 --- a/apps/desktop/src/lib/editor/shortcutRegistry.ts +++ b/apps/desktop/src/lib/editor/shortcutRegistry.ts @@ -29,6 +29,7 @@ export type ShortcutActionId = | "selectAllSelectionOccurrences" | "uppercaseSelection" | "lowercaseSelection" + | "toggleCaseSelection" | "convertNamingStyle" | "exPasteSqlInCondition" | "toggleFold" @@ -330,6 +331,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "editor", defaultShortcut: "Shift+Alt+L", }, + { + id: "toggleCaseSelection", + labelKey: "settings.shortcutToggleCaseSelection", + scope: "editor", + defaultShortcut: "Mod+Shift+U", + }, { id: "convertNamingStyle", labelKey: "settings.shortcutConvertNamingStyle", diff --git a/apps/desktop/src/lib/sql/sqlSelectionCase.ts b/apps/desktop/src/lib/sql/sqlSelectionCase.ts index 325d98b84d..47d4c969d9 100644 --- a/apps/desktop/src/lib/sql/sqlSelectionCase.ts +++ b/apps/desktop/src/lib/sql/sqlSelectionCase.ts @@ -1,19 +1,27 @@ import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens"; -export type SqlSelectionCaseMode = "upper" | "lower"; +export type SqlSelectionCaseMode = "upper" | "lower" | "toggle"; type SqlSelectionRange = { from: number; to: number; }; -function convertCase(text: string, mode: SqlSelectionCaseMode): string { +function convertCase(text: string, mode: "upper" | "lower"): string { return mode === "upper" ? text.toUpperCase() : text.toLowerCase(); } export function convertSqlSelectionCase(sql: string, range: SqlSelectionRange, mode: SqlSelectionCaseMode, dialectId?: "mysql" | "postgres" | "sqlserver"): string { const from = Math.max(0, Math.min(range.from, sql.length)); const to = Math.max(from, Math.min(range.to, sql.length)); + if (mode === "toggle") { + const upperResult = convertSqlSelectionCase(sql, range, "upper", dialectId); + const currentText = sql.slice(from, to); + if (upperResult !== currentText) { + return upperResult; + } + return convertSqlSelectionCase(sql, range, "lower", dialectId); + } const protectedTokens = tokenizeSqlSemantic(sql, dialectId).filter((item) => { if (item.span.end <= from || item.span.start >= to) return false; if (item.kind === "string") return true; diff --git a/docs/content/docs/keyboard-shortcuts.cn.mdx b/docs/content/docs/keyboard-shortcuts.cn.mdx index 9a415460ab..99cb56ca03 100644 --- a/docs/content/docs/keyboard-shortcuts.cn.mdx +++ b/docs/content/docs/keyboard-shortcuts.cn.mdx @@ -24,6 +24,7 @@ DBX 的快捷键来自同一份可配置 action registry。**设置 → 快捷 | `Shift+Mod+K` | 删除当前行 | | `Alt+↑` / `Alt+↓` | 上下移动当前行 | | `Shift+Alt+↑` / `Shift+Alt+↓` | 向上/下复制当前行 | +| `Mod+Shift+U` | 切换选中内容大小写(首次默认转为大写) | | `Shift+Alt+U` / `Shift+Alt+L` | 选中内容转大写/小写 | | `Shift+Alt+C` | 切换选中内容的命名风格(camelCase → PascalCase → snake_case → SCREAMING_SNAKE_CASE → kebab-case) | | `Mod+.` | 折叠或展开代码块 | diff --git a/docs/content/docs/keyboard-shortcuts.mdx b/docs/content/docs/keyboard-shortcuts.mdx index 9a990c9492..84420296a9 100644 --- a/docs/content/docs/keyboard-shortcuts.mdx +++ b/docs/content/docs/keyboard-shortcuts.mdx @@ -24,6 +24,7 @@ DBX shortcuts come from one configurable action registry. **Settings → Shortcu | `Shift+Mod+K` | Delete current line | | `Alt+↑` / `Alt+↓` | Move current line up/down | | `Shift+Alt+↑` / `Shift+Alt+↓` | Copy current line up/down | +| `Mod+Shift+U` | Toggle selection case (defaults to uppercase first) | | `Shift+Alt+U` / `Shift+Alt+L` | Convert selection to uppercase/lowercase | | `Shift+Alt+C` | Toggle selection naming style (camelCase → PascalCase → snake_case → SCREAMING_SNAKE_CASE → kebab-case) | | `Mod+.` | Toggle code folding |