mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(editor): add configurable line-end multi-cursors
This commit is contained in:
@@ -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);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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<NacosConfigDiagnostic[]>();
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Compartment } from "@codemirror/state";
|
||||
import { keymap as codeMirrorKeymap } from "@codemirror/view";
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
|
||||
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from "vue";
|
||||
@@ -6,6 +8,9 @@ import { useI18n } from "vue-i18n";
|
||||
import { Clipboard, ExternalLink, Loader2, Maximize2, Minimize2, RefreshCw } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
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 { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
@@ -21,7 +26,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { HelpTooltip } from "@/components/ui/tooltip";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import type { DatabaseType, ObjectSourceKind } from "@/types/database";
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -61,6 +66,8 @@ const ddlError = ref("");
|
||||
const ddlEditorContainer = ref<HTMLDivElement>();
|
||||
const ddlSearchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const ddlEditorView = shallowRef<EditorView | null>(null);
|
||||
const ddlEditorShortcut = new Compartment();
|
||||
const selectLineEndsShortcut = () => settingsStore.editorSettings?.shortcuts?.selectLineEnds ?? selectLineEndsDefaultShortcut();
|
||||
let ddlEditorResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// Keep the dialog movable for the duration of one open cycle. This mirrors the
|
||||
@@ -367,6 +374,10 @@ function onDdlDialogCloseAutoFocus(event: Event) {
|
||||
* - Prec.highest overrides Cmd+F to open EditorSearchPanel.
|
||||
* - Editor theme/font are loaded from user settings for consistent appearance.
|
||||
*/
|
||||
watch(selectLineEndsShortcut, (shortcut) => {
|
||||
ddlEditorView.value?.dispatch({ effects: ddlEditorShortcut.reconfigure(codeMirrorKeymap.of([{ key: shortcutToCodeMirrorKey(shortcut), preventDefault: true, run: selectLineEnds }])) });
|
||||
});
|
||||
|
||||
async function initDdlEditor(content: string) {
|
||||
if (!ddlEditorContainer.value) return;
|
||||
destroyDdlEditor();
|
||||
@@ -399,6 +410,14 @@ async function initDdlEditor(content: string) {
|
||||
fontExt,
|
||||
// Intercept Cmd+F at highest precedence so EditorSearchPanel opens
|
||||
// instead of the default search panel (which is hidden above).
|
||||
ddlEditorShortcut.of(keymap.of([{ key: shortcutToCodeMirrorKey(selectLineEndsShortcut()), preventDefault: true, run: selectLineEnds }])),
|
||||
EditorView.domEventHandlers({
|
||||
keydown(event, eventView) {
|
||||
if (!matchesShortcut(event, selectLineEndsShortcut())) return false;
|
||||
event.preventDefault();
|
||||
return selectLineEnds(eventView);
|
||||
},
|
||||
}),
|
||||
Prec.highest(keymap.of([{ key: "Mod-f", run: () => ddlSearchPanelRef.value?.openSearch() ?? false, preventDefault: true }])),
|
||||
// Remove CodeMirror's default 1px dotted focus outline,
|
||||
// which is visible below the content when the DDL is short.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Compartment } from "@codemirror/state";
|
||||
import { keymap as codeMirrorKeymap } from "@codemirror/view";
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
|
||||
import StructureIndexColumnPicker from "./StructureIndexColumnPicker.vue";
|
||||
@@ -26,6 +28,9 @@ import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { matchesShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { selectLineEndsDefaultShortcut, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { selectLineEnds } from "@/lib/editor/selectLineEnds";
|
||||
import { useSettingsStore, type StructureEditorDensity } from "@/stores/settingsStore";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
@@ -139,7 +144,7 @@ import {
|
||||
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, normalizeCreateDatabaseCharsetKey, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import type { CreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
@@ -248,6 +253,8 @@ const ddlEditorContainer = ref<HTMLDivElement>();
|
||||
const ddlSearchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const ddlSearchOpen = ref(false);
|
||||
const ddlEditorView = shallowRef<EditorView | null>(null);
|
||||
const ddlEditorShortcut = new Compartment();
|
||||
const selectLineEndsShortcut = () => settingsStore.editorSettings?.shortcuts?.selectLineEnds ?? selectLineEndsDefaultShortcut();
|
||||
let ddlEditorInitRequestId = 0;
|
||||
let ddlEditorScrollCleanup: (() => void) | null = null;
|
||||
const loadedMetadataFacets = new Set<ObjectMetadataFacet>();
|
||||
@@ -344,6 +351,10 @@ function observeDdlEditorScroll(view: EditorView) {
|
||||
ddlEditorScrollCleanup = () => scrollDOM.removeEventListener("scroll", onScroll);
|
||||
}
|
||||
|
||||
watch(selectLineEndsShortcut, (shortcut) => {
|
||||
ddlEditorView.value?.dispatch({ effects: ddlEditorShortcut.reconfigure(codeMirrorKeymap.of([{ key: shortcutToCodeMirrorKey(shortcut), preventDefault: true, run: selectLineEnds }])) });
|
||||
});
|
||||
|
||||
async function initDdlEditor(content: string) {
|
||||
const container = ddlEditorContainer.value;
|
||||
if (!container) return;
|
||||
@@ -387,6 +398,14 @@ async function initDdlEditor(content: string) {
|
||||
langSql.sql({ dialect }),
|
||||
themeExt,
|
||||
fontExt,
|
||||
ddlEditorShortcut.of(keymap.of([{ key: shortcutToCodeMirrorKey(selectLineEndsShortcut()), preventDefault: true, run: selectLineEnds }])),
|
||||
EditorView.domEventHandlers({
|
||||
keydown(event, eventView) {
|
||||
if (!matchesShortcut(event, selectLineEndsShortcut())) return false;
|
||||
event.preventDefault();
|
||||
return selectLineEnds(eventView);
|
||||
},
|
||||
}),
|
||||
Prec.highest(keymap.of([{ key: "Mod-f", run: () => ddlSearchPanelRef.value?.openSearch() ?? false, preventDefault: true }])),
|
||||
EditorView.theme({
|
||||
"&.cm-focused": { outline: "none" },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { bracketMatching, foldGutter, foldKeymap } from "@codemirror/language";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, cellDetailActiveLineColor, loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
import { matchesShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { CELL_DETAIL_JSON_FORMAT_MAX_LENGTH, isJsonColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
@@ -17,6 +18,7 @@ import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import type { EditorTheme } from "@/stores/settingsStore";
|
||||
import type { AppThemeAppearance, AppThemePalette } from "@/lib/app/appTheme";
|
||||
import { selectAllCellDetailText } from "@/lib/dataGrid/cellDetailSelection";
|
||||
import { selectLineEnds } from "@/lib/editor/selectLineEnds";
|
||||
|
||||
export interface UseCellDetailEditorOptions {
|
||||
onChange?: (value: string) => void;
|
||||
@@ -79,6 +81,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
||||
const fontThemeComp = new Compartment();
|
||||
const lineWrappingComp = new Compartment();
|
||||
const readOnlyComp = new Compartment();
|
||||
const shortcutComp = new Compartment();
|
||||
|
||||
let destroyed = false;
|
||||
let currentIsJson = false;
|
||||
@@ -185,6 +188,15 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.shortcuts.selectLineEnds,
|
||||
(shortcut) => {
|
||||
const editor = view.value;
|
||||
if (!editor || destroyed) return;
|
||||
editor.dispatch({ effects: shortcutComp.reconfigure(keymap.of([{ key: shortcutToCodeMirrorKey(shortcut), preventDefault: true, run: selectLineEnds }])) });
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => isReadOnly(),
|
||||
(readOnly) => {
|
||||
@@ -258,6 +270,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
||||
readOnlyComp.of(readOnlyExtensions(isReadOnly())),
|
||||
themeComp.of(theme),
|
||||
fontThemeComp.of(fontTheme),
|
||||
shortcutComp.of(keymap.of([{ key: shortcutToCodeMirrorKey(shortcuts.selectLineEnds), preventDefault: true, run: selectLineEnds }])),
|
||||
keymap.of([
|
||||
{
|
||||
key: "Mod-a",
|
||||
@@ -278,7 +291,11 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
||||
}
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
keydown(event) {
|
||||
keydown(event, eventView) {
|
||||
if (matchesShortcut(event, settingsStore.editorSettings.shortcuts.selectLineEnds)) {
|
||||
event.preventDefault();
|
||||
return selectLineEnds(eventView);
|
||||
}
|
||||
if (!options.onSaveShortcut?.(event)) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -8650,6 +8650,7 @@ export default {
|
||||
shortcutSelectAll: "Select all",
|
||||
shortcutAddNextSelectionOccurrence: "Add next occurrence",
|
||||
shortcutSelectAllSelectionOccurrences: "Select all occurrences",
|
||||
shortcutSelectLineEnds: "Select line ends",
|
||||
shortcutExtendSelection: "Extend selection",
|
||||
shortcutEditTableStructure: "Edit table structure",
|
||||
shortcutCopyCurrentRow: "Copy current data row",
|
||||
|
||||
@@ -8526,6 +8526,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "Proveedores de IA",
|
||||
sidebarSearchOpenedDatabasesOnly: "Buscar solo en bases de datos abiertas",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "La búsqueda de la barra lateral solo carga las bases de datos abiertas en la conexión actual (si no hay ninguna abierta, busca en todas). Al desactivarla, se buscará en todas las bases de datos de esa conexión.",
|
||||
shortcutSelectLineEnds: "Seleccionar finales de línea",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "No se pudo eliminar el directorio JRE antiguo: {path} (error original: {error})",
|
||||
|
||||
@@ -8272,6 +8272,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "Provider AI",
|
||||
sidebarSearchOpenedDatabasesOnly: "Cerca solo nei database aperti",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "La ricerca nella barra laterale carica solo i database aperti nella connessione corrente (se nessuno è aperto, cerca in tutti). Se disattivata, cerca in tutti i database della connessione.",
|
||||
shortcutSelectLineEnds: "Seleziona fine riga",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Impossibile rimuovere la vecchia directory JRE: {path} (errore originale: {error})",
|
||||
|
||||
@@ -8314,6 +8314,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "AIプロバイダー",
|
||||
sidebarSearchOpenedDatabasesOnly: "開いているデータベースのみを検索",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "サイドバー検索は、現在の接続で開いているデータベースのみを読み込みます(1つも開いていない場合はすべてを検索します)。オフにすると、その接続のすべてのデータベースを検索します。",
|
||||
shortcutSelectLineEnds: "行末を選択",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})",
|
||||
|
||||
@@ -8048,6 +8048,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "AI 제공자",
|
||||
sidebarSearchOpenedDatabasesOnly: "열려 있는 데이터베이스만 검색",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "사이드바 검색은 현재 연결에서 열려 있는 데이터베이스만 로드합니다(열려 있는 데이터베이스가 하나도 없으면 전체를 검색). 끄면 해당 연결의 모든 데이터베이스를 검색합니다.",
|
||||
shortcutSelectLineEnds: "줄 끝 선택",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "이전 JRE 디렉터리 제거 실패: {path} (원래 오류: {error})",
|
||||
|
||||
@@ -8273,6 +8273,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "Provedores de IA",
|
||||
sidebarSearchOpenedDatabasesOnly: "Pesquisar apenas bancos de dados abertos",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "A pesquisa da barra lateral carrega apenas os bancos de dados abertos na conexão atual (se nenhum estiver aberto, pesquisa todos). Quando desativada, pesquisará todos os bancos de dados da conexão.",
|
||||
shortcutSelectLineEnds: "Selecionar fins de linha",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Não foi possível remover o diretório JRE antigo: {path} (erro original: {error})",
|
||||
|
||||
@@ -9492,6 +9492,7 @@ export default withEnglishFallback({
|
||||
resultTabPreferCommentsDescription: "Использовать комментарий над SQL-запросом; отключите, чтобы всегда отображать имя исходной таблицы.",
|
||||
sidebarSearchOpenedDatabasesOnly: "Искать только в открытых базах данных",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "Поиск на боковой панели загружает только открытые базы данных в текущем подключении (если ни одна не открыта, поиск выполняется по всем). Если отключить, поиск будет выполняться по всем базам данных этого подключения.",
|
||||
shortcutSelectLineEnds: "Выделить концы строк",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "Не удалось удалить старый каталог JRE: {path} (исходная ошибка: {error})",
|
||||
|
||||
@@ -8632,6 +8632,7 @@ export default withEnglishFallback({
|
||||
shortcutSelectAll: "全选",
|
||||
shortcutAddNextSelectionOccurrence: "增加下一个选中词",
|
||||
shortcutSelectAllSelectionOccurrences: "选中所有选中词",
|
||||
shortcutSelectLineEnds: "选中行尾",
|
||||
shortcutExtendSelection: "扩展选择",
|
||||
shortcutUppercaseSelection: "选中内容转为大写",
|
||||
shortcutLowercaseSelection: "选中内容转为小写",
|
||||
|
||||
@@ -7628,6 +7628,7 @@ export default withEnglishFallback({
|
||||
supportInfoAiProviders: "AI 供應商",
|
||||
sidebarSearchOpenedDatabasesOnly: "僅搜尋已開啟的資料庫",
|
||||
sidebarSearchOpenedDatabasesOnlyDescription: "側邊欄搜尋只載入目前連線中已開啟的資料庫(若一個都沒開啟則搜尋全部)。關閉後會搜尋該連線的全部資料庫。",
|
||||
shortcutSelectLineEnds: "選取行尾",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "無法刪除舊的 JRE 目錄:{path}(原始錯誤:{error})",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
normalizeShortcutSettings,
|
||||
resolveCapturedShortcutEdit,
|
||||
selectionOccurrenceDefaultShortcut,
|
||||
selectLineEndsDefaultShortcut,
|
||||
SHORTCUT_DEFINITIONS,
|
||||
shortcutToCodeMirrorKey,
|
||||
toggleAiPanelDefaultShortcut,
|
||||
@@ -333,6 +334,14 @@ describe("shortcutRegistry editor actions", () => {
|
||||
expect(findShortcutConflict("selectAllSelectionOccurrences", DEFAULT_SHORTCUT_SETTINGS.selectAllSelectionOccurrences, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("registers the VS Code-style select-line-ends shortcut", () => {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "selectLineEnds");
|
||||
expect(definition).toMatchObject({ scope: "editor", defaultShortcut: "Alt+Shift+I", labelKey: "settings.shortcutSelectLineEnds" });
|
||||
expect(selectLineEndsDefaultShortcut("MacIntel")).toBe("Alt+Shift+I");
|
||||
expect(selectLineEndsDefaultShortcut("Win32")).toBe("Alt+Shift+I");
|
||||
expect(shortcutToCodeMirrorKey(selectLineEndsDefaultShortcut("MacIntel"))).toBe("Alt-Shift-i");
|
||||
});
|
||||
|
||||
it("resolves occurrence selection defaults per platform", () => {
|
||||
expect(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", "MacIntel")).toBe("Ctrl+G");
|
||||
expect(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", "MacIntel")).toBe("Ctrl+Mod+G");
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { selectionLineEndCursors } from "./selectLineEnds";
|
||||
|
||||
describe("selectionLineEndCursors", () => {
|
||||
it("selects the end of every touched line", () => {
|
||||
const state = EditorState.create({ doc: "one\ntwo\nthree" });
|
||||
const selection = EditorSelection.single(1, 9);
|
||||
const result = selectionLineEndCursors(state, selection.ranges);
|
||||
expect(result.ranges.map((range) => range.head)).toEqual([3, 7, 13]);
|
||||
});
|
||||
|
||||
it("uses the current line for an empty selection", () => {
|
||||
const state = EditorState.create({ doc: "one\ntwo\nthree" });
|
||||
const result = selectionLineEndCursors(state, [EditorSelection.cursor(5)]);
|
||||
expect(result.ranges.map((range) => range.head)).toEqual([7]);
|
||||
});
|
||||
|
||||
it("deduplicates lines covered by multiple ranges", () => {
|
||||
const state = EditorState.create({ doc: "one\ntwo\nthree" });
|
||||
const selection = EditorSelection.create([EditorSelection.range(0, 5), EditorSelection.range(4, 9)]);
|
||||
const result = selectionLineEndCursors(state, selection.ranges);
|
||||
expect(result.ranges.map((range) => range.head)).toEqual([3, 7, 13]);
|
||||
});
|
||||
|
||||
it("handles an empty document", () => {
|
||||
const state = EditorState.create({ doc: "" });
|
||||
const result = selectionLineEndCursors(state);
|
||||
expect(result.ranges.map((range) => range.head)).toEqual([0]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { EditorSelection, type EditorState, type SelectionRange } from "@codemirror/state";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
|
||||
/** Return one cursor at the end of every document line touched by a selection. */
|
||||
export function selectionLineEndCursors(state: EditorState, ranges: readonly SelectionRange[] = state.selection.ranges): EditorSelection {
|
||||
const positions = new Set<number>();
|
||||
for (const range of ranges) {
|
||||
const fromLine = state.doc.lineAt(range.from);
|
||||
const toLine = state.doc.lineAt(range.to);
|
||||
for (let number = fromLine.number; number <= toLine.number; number++) {
|
||||
positions.add(state.doc.line(number).to);
|
||||
}
|
||||
}
|
||||
const cursors = [...positions].sort((a, b) => a - b).map((position) => EditorSelection.cursor(position));
|
||||
return EditorSelection.create(cursors.length ? cursors : [EditorSelection.cursor(0)]);
|
||||
}
|
||||
|
||||
/** Place cursors at the ends of all lines touched by the current selection. */
|
||||
export function selectLineEnds(view: EditorView): boolean {
|
||||
view.dispatch({ selection: selectionLineEndCursors(view.state), userEvent: "select.lineEnds" });
|
||||
return true;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export type ShortcutActionId =
|
||||
| "extendSelection"
|
||||
| "addNextSelectionOccurrence"
|
||||
| "selectAllSelectionOccurrences"
|
||||
| "selectLineEnds"
|
||||
| "uppercaseSelection"
|
||||
| "lowercaseSelection"
|
||||
| "toggleCaseSelection"
|
||||
@@ -121,6 +122,12 @@ export function selectionOccurrenceDefaultShortcut(actionId: "addNextSelectionOc
|
||||
return actionId === "addNextSelectionOccurrence" ? "Alt+J" : "Ctrl+Alt+Shift+J";
|
||||
}
|
||||
|
||||
export function selectLineEndsDefaultShortcut(_platform = globalThis.navigator?.platform || ""): string {
|
||||
// VS Code uses Option+Shift+I on macOS; CodeMirror's physical shortcut
|
||||
// representation is Alt+Shift+I on every platform.
|
||||
return "Alt+Shift+I";
|
||||
}
|
||||
|
||||
export function tabNavigationHistoryDefaultShortcut(direction: "back" | "forward", platform = globalThis.navigator?.platform || ""): string {
|
||||
const modifier = isMacShortcutPlatform(platform) ? "Ctrl" : "Mod";
|
||||
const key = direction === "back" ? "ArrowLeft" : "ArrowRight";
|
||||
@@ -152,6 +159,7 @@ const PLATFORM_DEFAULT_SHORTCUTS: Partial<Record<ShortcutActionId, ReadonlySet<s
|
||||
navigateTabHistoryForward: new Set(["Ctrl+Alt+ArrowRight", "Mod+Alt+ArrowRight"]),
|
||||
addNextSelectionOccurrence: new Set(["Ctrl+G", "Alt+J"]),
|
||||
selectAllSelectionOccurrences: new Set(["Ctrl+Mod+G", "Ctrl+Alt+Shift+J"]),
|
||||
selectLineEnds: new Set(["Alt+Shift+I"]),
|
||||
toggleAiPanel: new Set(["Ctrl+Mod+I", "Ctrl+Alt+I"]),
|
||||
gotoLine: new Set(["Mod+G", "Mod+Alt+G"]),
|
||||
foldAll: new Set(["Mod+Alt+[", "Ctrl+Alt+[", "Shift+Alt+["]),
|
||||
@@ -319,6 +327,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
||||
scope: "editor",
|
||||
defaultShortcut: "Ctrl+Mod+G",
|
||||
},
|
||||
{
|
||||
id: "selectLineEnds",
|
||||
labelKey: "settings.shortcutSelectLineEnds",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Alt+Shift+I",
|
||||
},
|
||||
{
|
||||
id: "uppercaseSelection",
|
||||
labelKey: "settings.shortcutUppercaseSelection",
|
||||
@@ -709,6 +723,7 @@ function shortcutsUseSameKeys(first: string, second: string, platform = globalTh
|
||||
function shortcutDefaultForPlatform(definition: ShortcutDefinition, platform: string): string {
|
||||
if (definition.id === "addNextSelectionOccurrence") return selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", platform);
|
||||
if (definition.id === "selectAllSelectionOccurrences") return selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", platform);
|
||||
if (definition.id === "selectLineEnds") return selectLineEndsDefaultShortcut(platform);
|
||||
if (definition.id === "closeOtherTabs") return closeOtherTabsDefaultShortcut(platform);
|
||||
if (definition.id === "gotoLine") return gotoLineDefaultShortcut(platform);
|
||||
if (definition.id === "navigateTabHistoryBack") return tabNavigationHistoryDefaultShortcut("back", platform);
|
||||
|
||||
@@ -489,3 +489,10 @@ test("matches configurable sidebar shortcuts", () => {
|
||||
assert.equal(isDisconnectSidebarConnectionShortcut({ key: "e", metaKey: true, shiftKey: true }, shortcuts), false);
|
||||
assert.equal(isDisconnectSidebarConnectionShortcut({ key: "d", altKey: true }, shortcuts), true);
|
||||
});
|
||||
|
||||
test("defaults and normalizes select-line-ends shortcut", () => {
|
||||
const defaults = DEFAULT_SHORTCUT_SETTINGS;
|
||||
assert.equal(defaults.selectLineEnds, "Alt+Shift+I");
|
||||
assert.equal(normalizeShortcutSettings({ selectLineEnds: "Shift+Alt+L" }).selectLineEnds, "Shift+Alt+L");
|
||||
assert.equal(shortcutToCodeMirrorKey(defaults.selectLineEnds), "Alt-Shift-i");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user