feat(editor): add toggle case shortcut for selection

This commit is contained in:
lxk955
2026-10-04 10:02:06 +08:00
committed by GitHub
parent eeeea648c1
commit f47c209b15
21 changed files with 111 additions and 3 deletions
@@ -2124,6 +2124,7 @@ const formatterEditorShortcutIds: ShortcutActionId[] = [
"selectAll",
"uppercaseSelection",
"lowercaseSelection",
"toggleCaseSelection",
"toggleFold",
"foldAll",
"unfoldAll",
@@ -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) => {
@@ -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<ContextMenuItem[]>(() => {
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"),
+2
View File
@@ -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",
},
+2
View File
@@ -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",
},
+2
View File
@@ -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",
+2
View File
@@ -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",
},
+2
View File
@@ -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",
+2
View File
@@ -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: "グローバル表示形式",
+2
View File
@@ -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: "모두 접기",
+2
View File
@@ -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",
+2
View File
@@ -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: "Всплывающие подсказки при наведении на имена полей",
+2
View File
@@ -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: {
+2
View File
@@ -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: "编辑表结构",
+2
View File
@@ -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: "資料標籤頁",
@@ -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");
@@ -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')");
});
});
});
@@ -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",
+10 -2
View File
@@ -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;
@@ -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+.` | 折叠或展开代码块 |
+1
View File
@@ -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 |