feat(editor): add occurrence selection actions
This commit is contained in:
1 parent
91f309f6c7
commit
7d92433689
13 files changed
+284
-1
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import { EditorSelection, type EditorState } from "@codemirror/state";
|
||||
import { SearchCursor, selectNextOccurrence } from "@codemirror/search";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
|
||||
const MAX_OCCURRENCE_SELECTIONS = 1000;
|
||||
|
||||
function selectionOccurrenceQuery(state: EditorState): string | null {
|
||||
const ranges = state.selection.ranges;
|
||||
if (ranges.length === 1 && ranges[0].empty) {
|
||||
const word = state.wordAt(ranges[0].head);
|
||||
return word ? state.sliceDoc(word.from, word.to) : null;
|
||||
}
|
||||
|
||||
const query = state.sliceDoc(ranges[0].from, ranges[0].to);
|
||||
if (!query.trim()) return null;
|
||||
return ranges.every((range) => !range.empty && state.sliceDoc(range.from, range.to) === query) ? query : null;
|
||||
}
|
||||
|
||||
export function addNextQueryEditorSelectionOccurrence(view: EditorView): boolean {
|
||||
const ranges = view.state.selection.ranges;
|
||||
const hasEmptyRange = ranges.some((range) => range.empty);
|
||||
if (hasEmptyRange && ranges.length > 1) {
|
||||
// The single-cursor word-expansion helper cannot run with multiple
|
||||
// cursors; CodeMirror's native command word-expands every empty range
|
||||
// and adds the next occurrence of the shared query.
|
||||
return selectNextOccurrence(view);
|
||||
}
|
||||
if (hasEmptyRange && !selectCurrentWord(view)) return false;
|
||||
return selectNextOccurrence(view);
|
||||
}
|
||||
|
||||
export function selectAllQueryEditorSelectionOccurrences(view: EditorView): boolean {
|
||||
const query = selectionOccurrenceQuery(view.state);
|
||||
if (!query) return false;
|
||||
|
||||
const ranges = [];
|
||||
let mainIndex = 0;
|
||||
const main = view.state.selection.main;
|
||||
const cursor = new SearchCursor(view.state.doc, query);
|
||||
for (let match = cursor.next(); !match.done; match = cursor.next()) {
|
||||
if (ranges.length >= MAX_OCCURRENCE_SELECTIONS) return false;
|
||||
if (match.value.from === main.from) mainIndex = ranges.length;
|
||||
ranges.push(EditorSelection.range(match.value.from, match.value.to));
|
||||
}
|
||||
if (!ranges.length) return false;
|
||||
view.dispatch({ selection: EditorSelection.create(ranges, mainIndex), userEvent: "select.search.matches" });
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectCurrentWord(view: EditorView): boolean {
|
||||
const selection = view.state.selection;
|
||||
if (selection.ranges.length !== 1 || !selection.main.empty) return false;
|
||||
|
||||
const word = view.state.wordAt(selection.main.head);
|
||||
if (!word || word.from === word.to) return false;
|
||||
|
||||
view.dispatch({ selection: EditorSelection.single(word.from, word.to), scrollIntoView: true, userEvent: "select.search" });
|
||||
return true;
|
||||
}
|
||||
@@ -24,6 +24,8 @@ export type ShortcutActionId =
|
||||
| "redo"
|
||||
| "selectAll"
|
||||
| "extendSelection"
|
||||
| "addNextSelectionOccurrence"
|
||||
| "selectAllSelectionOccurrences"
|
||||
| "uppercaseSelection"
|
||||
| "lowercaseSelection"
|
||||
| "exPasteSqlInCondition"
|
||||
@@ -98,6 +100,17 @@ export function closeOtherTabsDefaultShortcut(platform = globalThis.navigator?.p
|
||||
return isMacShortcutPlatform(platform) ? "Alt+Mod+W" : "Shift+Alt+W";
|
||||
}
|
||||
|
||||
// 同词项选择的平台相关默认键。Windows/Linux 上 Ctrl+Mod+G 经 CodeMirror 的
|
||||
// Mod→Ctrl 展开后成为不可达的 Ctrl-Ctrl-G,且 Ctrl+G 本身是编辑器内
|
||||
// find-next(Mod-G 的非 mac 展开),因此非 mac 平台改用 JetBrains 风格的
|
||||
// Alt+J / Ctrl+Alt+Shift+J;macOS 维持 Ctrl+G / Ctrl+Cmd+G。
|
||||
export function selectionOccurrenceDefaultShortcut(actionId: "addNextSelectionOccurrence" | "selectAllSelectionOccurrences", platform = globalThis.navigator?.platform || ""): string {
|
||||
if (isMacShortcutPlatform(platform)) {
|
||||
return actionId === "addNextSelectionOccurrence" ? "Ctrl+G" : "Ctrl+Mod+G";
|
||||
}
|
||||
return actionId === "addNextSelectionOccurrence" ? "Alt+J" : "Ctrl+Alt+Shift+J";
|
||||
}
|
||||
|
||||
export function tabNavigationHistoryDefaultShortcut(direction: "back" | "forward", platform = globalThis.navigator?.platform || ""): string {
|
||||
const modifier = isMacShortcutPlatform(platform) ? "Ctrl" : "Mod";
|
||||
const key = direction === "back" ? "ArrowLeft" : "ArrowRight";
|
||||
@@ -108,6 +121,8 @@ const PLATFORM_DEFAULT_SHORTCUTS: Partial<Record<ShortcutActionId, ReadonlySet<s
|
||||
closeOtherTabs: new Set(["Alt+Mod+W", "Shift+Alt+W"]),
|
||||
navigateTabHistoryBack: new Set(["Ctrl+Alt+ArrowLeft", "Mod+Alt+ArrowLeft"]),
|
||||
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"]),
|
||||
};
|
||||
const LEGACY_CLOSE_TAB_DEFAULT = "Meta+W";
|
||||
const LEGACY_COPY_CURRENT_ROW_DEFAULT = "Mod+D";
|
||||
@@ -252,6 +267,18 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
||||
scope: "editor",
|
||||
defaultShortcut: "Alt+W",
|
||||
},
|
||||
{
|
||||
id: "addNextSelectionOccurrence",
|
||||
labelKey: "settings.shortcutAddNextSelectionOccurrence",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Ctrl+G",
|
||||
},
|
||||
{
|
||||
id: "selectAllSelectionOccurrences",
|
||||
labelKey: "settings.shortcutSelectAllSelectionOccurrences",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Ctrl+Mod+G",
|
||||
},
|
||||
{
|
||||
id: "uppercaseSelection",
|
||||
labelKey: "settings.shortcutUppercaseSelection",
|
||||
@@ -574,6 +601,8 @@ 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 === "closeOtherTabs") return closeOtherTabsDefaultShortcut(platform);
|
||||
if (definition.id === "navigateTabHistoryBack") return tabNavigationHistoryDefaultShortcut("back", platform);
|
||||
if (definition.id === "navigateTabHistoryForward") return tabNavigationHistoryDefaultShortcut("forward", platform);
|
||||
|
||||
Reference in new issue
Block a user