From 8ca612d1f7edfe9d06ec3c952d34b17c3a657d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Sun, 4 Oct 2026 16:41:40 +0800 Subject: [PATCH] feat(editor): add configurable line-end multi-cursors --- .../src/components/editor/QueryEditor.vue | 7 +++++ .../components/nacos/NacosAdminConsole.vue | 17 +++++++++- .../src/components/objects/DdlViewDialog.vue | 21 ++++++++++++- .../structure/TableStructureEditor.vue | 21 ++++++++++++- .../src/composables/useCellDetailEditor.ts | 19 +++++++++++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/ko.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/ru.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../__tests__/editor/shortcutRegistry.spec.ts | 9 ++++++ .../src/lib/editor/selectLineEnds.spec.ts | 31 +++++++++++++++++++ apps/desktop/src/lib/editor/selectLineEnds.ts | 22 +++++++++++++ .../src/lib/editor/shortcutRegistry.ts | 15 +++++++++ packages/app-tests/keyboardShortcuts.test.ts | 7 +++++ 19 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/lib/editor/selectLineEnds.spec.ts create mode 100644 apps/desktop/src/lib/editor/selectLineEnds.ts diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 0d7b0ee7fe..456ac4f556 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -110,6 +110,7 @@ import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoo import { buildSqlShortcutExecutionSql, enabledSqlShortcutActions, resolveSqlShortcutForDatabase, uniqueSqlShortcutBindings } from "@/lib/sql/sqlShortcutActions"; import { resolveSqlShortcutTableToken } from "@/lib/sql/sqlShortcutTableTarget"; import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry"; +import { matchesShortcut } from "@/lib/editor/keyboardShortcuts"; import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer"; import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings"; import { applyVimConfig, isVimMappingCommand, loadVimConfig } from "@/lib/editor/vimConfig"; @@ -129,6 +130,7 @@ import { computePasteCaretResyncTarget } from "@/lib/editor/queryEditorPasteCare import { extendQueryEditorSelection, runQueryEditorAltExtendSelection } from "@/lib/editor/queryEditorExtendSelection"; import { addNextQueryEditorSelectionOccurrence, selectAllQueryEditorSelectionOccurrences } from "@/lib/editor/queryEditorOccurrenceSelection"; +import { selectLineEnds } from "@/lib/editor/selectLineEnds"; import { createQueryEditorStringMouseSelection } from "@/lib/editor/queryEditorStringMouseSelection"; import { createQueryEditorCompletionShortcutBindings } from "@/lib/editor/queryEditorCompletionShortcut"; import { createQueryEditorSelectionCaseShortcutBindings } from "@/lib/editor/queryEditorSelectionCaseShortcut"; @@ -1394,6 +1396,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") ...binding(shortcuts.extendSelection, extendQueryEditorSelectionForView), ...binding(shortcuts.addNextSelectionOccurrence, addNextQueryEditorSelectionOccurrence), ...binding(shortcuts.selectAllSelectionOccurrences, selectAllQueryEditorSelectionOccurrences), + ...binding(shortcuts.selectLineEnds, selectLineEnds), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")), ...createQueryEditorSelectionCaseShortcutBindings(shortcuts.toggleCaseSelection, () => convertSelectedSqlCase("toggle")), @@ -2077,6 +2080,10 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({ EditorView.domEventHandlers({ keydown(event, currentView) { const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts); + if (matchesShortcut(event, shortcuts.selectLineEnds)) { + event.preventDefault(); + return selectLineEnds(currentView); + } return runQueryEditorAltExtendSelection(event, shortcuts.extendSelection, currentView, extendQueryEditorSelectionForView); }, }), diff --git a/apps/desktop/src/components/nacos/NacosAdminConsole.vue b/apps/desktop/src/components/nacos/NacosAdminConsole.vue index f412bdfcaf..2ac9a4e7b5 100644 --- a/apps/desktop/src/components/nacos/NacosAdminConsole.vue +++ b/apps/desktop/src/components/nacos/NacosAdminConsole.vue @@ -4,7 +4,7 @@ import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMoun import { Compartment, StateEffect, StateField } from "@codemirror/state"; import { ensureSyntaxTree } from "@codemirror/language"; import { setDiagnostics } from "@codemirror/lint"; -import { Decoration, EditorView } from "@codemirror/view"; +import { Decoration, EditorView, keymap as codeMirrorKeymap } from "@codemirror/view"; import { Archive, ArrowLeftRight, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clipboard, Columns3, Download, ExternalLink, FileClock, FileInput, FileText, Loader2, Maximize2, Minimize2, Network, Plus, RefreshCw, ReplaceAll, Save, Search, Send, Server, Trash2, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -57,6 +57,9 @@ import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes"; import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoomCommitScheduler, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom"; import { replaceFallbackKey } from "@/lib/editor/queryEditorSearchKeymap"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; +import { matchesShortcut } from "@/lib/editor/keyboardShortcuts"; +import { selectLineEndsDefaultShortcut, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry"; +import { selectLineEnds } from "@/lib/editor/selectLineEnds"; import { useSettingsStore } from "@/stores/settingsStore"; import { useTheme } from "@/composables/useTheme"; import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard"; @@ -249,6 +252,8 @@ const configEditorFontTheme = new Compartment(); const configEditorWordWrap = new Compartment(); const configEditorLanguage = new Compartment(); const configValidationHighlight = new Compartment(); +const configEditorShortcut = new Compartment(); +const selectLineEndsShortcut = () => settingsStore.editorSettings?.shortcuts?.selectLineEnds ?? selectLineEndsDefaultShortcut(); const setConfigValidationHighlight = StateEffect.define(); const configListRequestGuard = createNacosLatestRequestGuard(); const configDetailRequestGuard = createNacosLatestRequestGuard(); @@ -593,6 +598,10 @@ function configValidationHighlightExtension() { return field; } +watch(selectLineEndsShortcut, (shortcut) => { + configEditorView.value?.dispatch({ effects: configEditorShortcut.reconfigure(codeMirrorKeymap.of([{ key: shortcutToCodeMirrorKey(shortcut), preventDefault: true, run: selectLineEnds }])) }); +}); + async function mountConfigEditor() { await nextTick(); if (!configEditorHost.value || configEditorView.value || !selectedConfig.value) return; @@ -627,9 +636,15 @@ async function mountConfigEditor() { basicSetup, EditorState.allowMultipleSelections.of(true), trimmedSelectionLayer(), + configEditorShortcut.of(keymap.of([{ key: shortcutToCodeMirrorKey(selectLineEndsShortcut()), preventDefault: true, run: selectLineEnds }])), Prec.highest(keymap.of([{ key: "Mod-f", run: () => configSearchPanelRef.value?.openSearch() ?? false, preventDefault: true }, { key: replaceFallbackKey(), run: () => configSearchPanelRef.value?.openReplace() ?? false, preventDefault: true }, indentWithTab])), keymap.of([...defaultKeymap, ...historyKeymap]), EditorView.domEventHandlers({ + keydown(event, eventView) { + if (!matchesShortcut(event, selectLineEndsShortcut())) return false; + event.preventDefault(); + return selectLineEnds(eventView); + }, wheel(event, eventView) { if (!configEditorWheelZoomGestureGuard.accepts(event)) return false; event.preventDefault(); diff --git a/apps/desktop/src/components/objects/DdlViewDialog.vue b/apps/desktop/src/components/objects/DdlViewDialog.vue index 0a7843ed6c..6c386f0689 100644 --- a/apps/desktop/src/components/objects/DdlViewDialog.vue +++ b/apps/desktop/src/components/objects/DdlViewDialog.vue @@ -1,4 +1,6 @@