mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(editor): add occurrence selection actions
This commit is contained in:
@@ -140,6 +140,7 @@ import { computePasteCaretResyncTarget } from "@/lib/editor/queryEditorPasteCare
|
||||
import { queryEditorCommentTokens, queryEditorLineCommentToken } from "@/lib/editor/queryEditorLineComment";
|
||||
import { createShellLineCommentHighlight } from "@/lib/editor/codemirrorShellLineCommentHighlight";
|
||||
import { extendQueryEditorSelection, runQueryEditorAltExtendSelection } from "@/lib/editor/queryEditorExtendSelection";
|
||||
import { addNextQueryEditorSelectionOccurrence, selectAllQueryEditorSelectionOccurrences } from "@/lib/editor/queryEditorOccurrenceSelection";
|
||||
import { createQueryEditorStringMouseSelection } from "@/lib/editor/queryEditorStringMouseSelection";
|
||||
import { createQueryEditorCompletionShortcutBindings } from "@/lib/editor/queryEditorCompletionShortcut";
|
||||
import { createQueryEditorSelectionCaseShortcutBindings } from "@/lib/editor/queryEditorSelectionCaseShortcut";
|
||||
@@ -1950,6 +1951,18 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
disabled: props.readOnly || !canCopySelectedSql.value,
|
||||
icon: List,
|
||||
},
|
||||
{
|
||||
label: t("editor.contextMenu.addNextSelectionOccurrence"),
|
||||
action: addNextSelectionOccurrenceFromContextMenu,
|
||||
icon: TextSelect,
|
||||
shortcut: shortcuts.addNextSelectionOccurrence,
|
||||
},
|
||||
{
|
||||
label: t("editor.contextMenu.selectAllSelectionOccurrences"),
|
||||
action: selectAllSelectionOccurrencesFromContextMenu,
|
||||
icon: TextSelect,
|
||||
shortcut: shortcuts.selectAllSelectionOccurrences,
|
||||
},
|
||||
{ label: "", separator: true },
|
||||
{
|
||||
label: t("editor.contextMenu.findReplace"),
|
||||
@@ -2114,6 +2127,8 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
...binding(shortcuts.redo, (view) => codeMirrorRedo?.(view) ?? false),
|
||||
...binding(shortcuts.selectAll, (view) => codeMirrorSelectAll?.(view) ?? false),
|
||||
...binding(shortcuts.extendSelection, extendQueryEditorSelectionForView),
|
||||
...binding(shortcuts.addNextSelectionOccurrence, addNextQueryEditorSelectionOccurrence),
|
||||
...binding(shortcuts.selectAllSelectionOccurrences, selectAllQueryEditorSelectionOccurrences),
|
||||
...createQueryEditorSelectionCaseShortcutBindings(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")),
|
||||
...createQueryEditorSelectionCaseShortcutBindings(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")),
|
||||
...binding(shortcuts.toggleLineComment, (view) => codeMirrorToggleLineComment?.(view) ?? false),
|
||||
@@ -2202,6 +2217,18 @@ function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean
|
||||
});
|
||||
}
|
||||
|
||||
function addNextSelectionOccurrenceFromContextMenu() {
|
||||
if (!view.value) return;
|
||||
addNextQueryEditorSelectionOccurrence(view.value);
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function selectAllSelectionOccurrencesFromContextMenu() {
|
||||
if (!view.value) return;
|
||||
selectAllQueryEditorSelectionOccurrences(view.value);
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
|
||||
// Any non-empty selection range means Tab is being used for block indent,
|
||||
// not word completion. A completion popup can still appear as a side effect
|
||||
|
||||
@@ -1372,6 +1372,8 @@ export default {
|
||||
uppercaseSelection: "Convert to uppercase",
|
||||
lowercaseSelection: "Convert to lowercase",
|
||||
delimitedList: "Convert to delimited list",
|
||||
addNextSelectionOccurrence: "Add next occurrence",
|
||||
selectAllSelectionOccurrences: "Select all occurrences",
|
||||
expandSelectStar: "Expand * to columns",
|
||||
expandSelectStarUnavailable: "Unable to load columns for this table",
|
||||
findReplace: "Find/Replace",
|
||||
@@ -6844,6 +6846,8 @@ export default {
|
||||
shortcutUndo: "Undo",
|
||||
shortcutRedo: "Redo",
|
||||
shortcutSelectAll: "Select all",
|
||||
shortcutAddNextSelectionOccurrence: "Add next occurrence",
|
||||
shortcutSelectAllSelectionOccurrences: "Select all occurrences",
|
||||
shortcutExtendSelection: "Extend selection",
|
||||
shortcutEditTableStructure: "Edit table structure",
|
||||
shortcutCopyCurrentRow: "Copy current data row",
|
||||
|
||||
@@ -1344,6 +1344,8 @@ export default withEnglishFallback({
|
||||
lowercaseSelection: "Convertir a minusculas",
|
||||
selectAll: "Seleccionar todo",
|
||||
delimitedList: "Convertir a lista delimitada",
|
||||
addNextSelectionOccurrence: "Añadir siguiente coincidencia",
|
||||
selectAllSelectionOccurrences: "Seleccionar todas las coincidencias",
|
||||
expandSelectStar: "Expandir * a columnas",
|
||||
expandSelectStarUnavailable: "No se pudieron cargar las columnas de esta tabla",
|
||||
findReplace: "Buscar/Reemplazar",
|
||||
@@ -6471,6 +6473,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "Deshacer",
|
||||
shortcutRedo: "Rehacer",
|
||||
shortcutSelectAll: "Seleccionar todo",
|
||||
shortcutAddNextSelectionOccurrence: "Añadir siguiente coincidencia",
|
||||
shortcutSelectAllSelectionOccurrences: "Seleccionar todas las coincidencias",
|
||||
shortcutExtendSelection: "Ampliar selección",
|
||||
shortcutEditTableStructure: "Editar estructura de tabla",
|
||||
shortcutCopyCurrentRow: "Copiar fila de datos actual",
|
||||
|
||||
@@ -1342,6 +1342,8 @@ export default withEnglishFallback({
|
||||
lowercaseSelection: "Converti in minuscolo",
|
||||
selectAll: "Seleziona tutto",
|
||||
delimitedList: "Converti in lista delimitata",
|
||||
addNextSelectionOccurrence: "Aggiungi occorrenza successiva",
|
||||
selectAllSelectionOccurrences: "Seleziona tutte le occorrenze",
|
||||
expandSelectStar: "Espandi * in colonne",
|
||||
expandSelectStarUnavailable: "Impossibile caricare le colonne di questa tabella",
|
||||
findReplace: "Trova/Sostituisci",
|
||||
@@ -6471,6 +6473,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "Annulla",
|
||||
shortcutRedo: "Ripeti",
|
||||
shortcutSelectAll: "Seleziona tutto",
|
||||
shortcutAddNextSelectionOccurrence: "Aggiungi occorrenza successiva",
|
||||
shortcutSelectAllSelectionOccurrences: "Seleziona tutte le occorrenze",
|
||||
shortcutExtendSelection: "Estendi selezione",
|
||||
shortcutEditTableStructure: "Modifica struttura tabella",
|
||||
shortcutCopyCurrentRow: "Copia riga dati corrente",
|
||||
|
||||
@@ -1362,6 +1362,8 @@ export default withEnglishFallback({
|
||||
lowercaseSelection: "小文字に変換",
|
||||
selectAll: "すべて選択",
|
||||
delimitedList: "区切り文字付きリストに変換",
|
||||
addNextSelectionOccurrence: "次の出現箇所を選択に追加",
|
||||
selectAllSelectionOccurrences: "すべての出現箇所を選択",
|
||||
expandSelectStar: "* を列に展開",
|
||||
expandSelectStarUnavailable: "このテーブルの列を読み込めません",
|
||||
findReplace: "検索/置換",
|
||||
@@ -6479,6 +6481,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "元に戻す",
|
||||
shortcutRedo: "やり直す",
|
||||
shortcutSelectAll: "すべて選択",
|
||||
shortcutAddNextSelectionOccurrence: "次の出現箇所を選択に追加",
|
||||
shortcutSelectAllSelectionOccurrences: "すべての出現箇所を選択",
|
||||
shortcutExtendSelection: "選択範囲を拡張",
|
||||
shortcutEditTableStructure: "テーブル構造を編集",
|
||||
shortcutCopyCurrentRow: "現在のデータ行をコピー",
|
||||
|
||||
@@ -1255,6 +1255,8 @@ export default withEnglishFallback({
|
||||
uppercaseSelection: "대문자로 변환",
|
||||
lowercaseSelection: "소문자로 변환",
|
||||
delimitedList: "구분자 목록으로 변환",
|
||||
addNextSelectionOccurrence: "다음 일치 항목 선택 추가",
|
||||
selectAllSelectionOccurrences: "모든 일치 항목 선택",
|
||||
expandSelectStar: "*을(를) 열로 확장",
|
||||
expandSelectStarUnavailable: "이 테이블의 열을 불러올 수 없습니다",
|
||||
findReplace: "찾기/바꾸기",
|
||||
@@ -6222,6 +6224,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "실행 취소",
|
||||
shortcutRedo: "다시 실행",
|
||||
shortcutSelectAll: "모두 선택",
|
||||
shortcutAddNextSelectionOccurrence: "다음 일치 항목 선택 추가",
|
||||
shortcutSelectAllSelectionOccurrences: "모든 일치 항목 선택",
|
||||
shortcutEditTableStructure: "테이블 구조 편집",
|
||||
shortcutCopyCurrentRow: "현재 데이터 행 복사",
|
||||
shortcutDeleteCurrentRow: "현재 데이터 행 삭제",
|
||||
|
||||
@@ -1343,6 +1343,8 @@ export default withEnglishFallback({
|
||||
lowercaseSelection: "Converter para minúsculas",
|
||||
selectAll: "Selecionar tudo",
|
||||
delimitedList: "Converter para lista delimitada",
|
||||
addNextSelectionOccurrence: "Adicionar próxima ocorrência",
|
||||
selectAllSelectionOccurrences: "Selecionar todas as ocorrências",
|
||||
expandSelectStar: "Expandir * em colunas",
|
||||
expandSelectStarUnavailable: "Não foi possível carregar as colunas desta tabela",
|
||||
findReplace: "Localizar/substituir",
|
||||
@@ -6473,6 +6475,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "Desfazer",
|
||||
shortcutRedo: "Refazer",
|
||||
shortcutSelectAll: "Selecionar tudo",
|
||||
shortcutAddNextSelectionOccurrence: "Adicionar próxima ocorrência",
|
||||
shortcutSelectAllSelectionOccurrences: "Selecionar todas as ocorrências",
|
||||
shortcutExtendSelection: "Expandir seleção",
|
||||
shortcutEditTableStructure: "Editar estrutura da tabela",
|
||||
shortcutCopyCurrentRow: "Copiar linha de dados atual",
|
||||
|
||||
@@ -1296,6 +1296,8 @@ export default withEnglishFallback({
|
||||
uppercaseSelection: "转为大写",
|
||||
lowercaseSelection: "转为小写",
|
||||
delimitedList: "转换为带分隔符的列表",
|
||||
addNextSelectionOccurrence: "增加下一个选中词",
|
||||
selectAllSelectionOccurrences: "选中所有选中词",
|
||||
expandSelectStar: "将 * 展开为字段",
|
||||
expandSelectStarUnavailable: "无法读取该表的字段信息",
|
||||
findReplace: "查找/替换",
|
||||
@@ -6825,6 +6827,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "撤销",
|
||||
shortcutRedo: "重做",
|
||||
shortcutSelectAll: "全选",
|
||||
shortcutAddNextSelectionOccurrence: "增加下一个选中词",
|
||||
shortcutSelectAllSelectionOccurrences: "选中所有选中词",
|
||||
shortcutExtendSelection: "扩展选择",
|
||||
shortcutUppercaseSelection: "选中内容转为大写",
|
||||
shortcutLowercaseSelection: "选中内容转为小写",
|
||||
|
||||
@@ -1342,6 +1342,8 @@ export default withEnglishFallback({
|
||||
lowercaseSelection: "轉為小寫",
|
||||
selectAll: "全選",
|
||||
delimitedList: "轉換為帶分隔符的清單",
|
||||
addNextSelectionOccurrence: "增加下一個選取詞",
|
||||
selectAllSelectionOccurrences: "選取所有選取詞",
|
||||
expandSelectStar: "將 * 展開為欄位",
|
||||
expandSelectStarUnavailable: "無法讀取此資料表的欄位資訊",
|
||||
findReplace: "查找/取代",
|
||||
@@ -5776,6 +5778,8 @@ export default withEnglishFallback({
|
||||
shortcutUndo: "復原",
|
||||
shortcutRedo: "重做",
|
||||
shortcutSelectAll: "全選",
|
||||
shortcutAddNextSelectionOccurrence: "增加下一個選取詞",
|
||||
shortcutSelectAllSelectionOccurrences: "選取所有選取詞",
|
||||
shortcutExtendSelection: "擴展選取範圍",
|
||||
shortcutEditTableStructure: "編輯資料表結構",
|
||||
shortcutCopyCurrentRow: "複製目前資料列",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { addNextQueryEditorSelectionOccurrence, selectAllQueryEditorSelectionOccurrences } from "@/lib/editor/queryEditorOccurrenceSelection";
|
||||
|
||||
function createView(doc: string, selection: EditorSelection) {
|
||||
return new EditorView({
|
||||
parent: document.createElement("div"),
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
selection,
|
||||
extensions: [EditorState.allowMultipleSelections.of(true)],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function ranges(view: EditorView) {
|
||||
return view.state.selection.ranges.map(({ from, to }) => ({ from, to }));
|
||||
}
|
||||
|
||||
describe("queryEditorOccurrenceSelection", () => {
|
||||
it("adds the next occurrence of the current selection", () => {
|
||||
const view = createView("foo bar foo baz foo", EditorSelection.single(0, 3));
|
||||
|
||||
expect(addNextQueryEditorSelectionOccurrence(view)).toBe(true);
|
||||
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 0, to: 3 },
|
||||
{ from: 8, to: 11 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("expands an empty cursor to the current word before adding the next occurrence", () => {
|
||||
const view = createView("select id from users where id = 1", EditorSelection.cursor(8));
|
||||
|
||||
expect(addNextQueryEditorSelectionOccurrence(view)).toBe(true);
|
||||
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 7, to: 9 },
|
||||
{ from: 27, to: 29 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("word-expands every empty range when multiple cursors are active", () => {
|
||||
// One cursor sits on a word, the second (Alt+click) is empty on another
|
||||
// word: the single-cursor helper cannot run, so the native command
|
||||
// word-expands every empty range and adds the next occurrence.
|
||||
const view = createView("foo bar foo", EditorSelection.create([EditorSelection.range(0, 3), EditorSelection.cursor(4)]));
|
||||
|
||||
expect(addNextQueryEditorSelectionOccurrence(view)).toBe(true);
|
||||
|
||||
// "bar" has no further occurrence, so the selection stays at the two
|
||||
// word ranges — the point is the empty cursor was expanded, not dropped.
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 0, to: 3 },
|
||||
{ from: 4, to: 7 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects every occurrence of the selected text", () => {
|
||||
const view = createView("foo bar foo baz foo", EditorSelection.single(0, 3));
|
||||
|
||||
expect(selectAllQueryEditorSelectionOccurrences(view)).toBe(true);
|
||||
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 0, to: 3 },
|
||||
{ from: 8, to: 11 },
|
||||
{ from: 16, to: 19 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects every occurrence after the next occurrence was already added", () => {
|
||||
const view = createView("foo bar foo baz foo", EditorSelection.single(0, 3));
|
||||
|
||||
expect(addNextQueryEditorSelectionOccurrence(view)).toBe(true);
|
||||
expect(selectAllQueryEditorSelectionOccurrences(view)).toBe(true);
|
||||
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 0, to: 3 },
|
||||
{ from: 8, to: 11 },
|
||||
{ from: 16, to: 19 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects all occurrences of the word around an empty cursor", () => {
|
||||
const view = createView("select id from users where id = 1", EditorSelection.cursor(8));
|
||||
|
||||
expect(selectAllQueryEditorSelectionOccurrences(view)).toBe(true);
|
||||
|
||||
expect(ranges(view)).toEqual([
|
||||
{ from: 7, to: 9 },
|
||||
{ from: 27, to: 29 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { closeOtherTabsDefaultShortcut, DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict, formatShortcut, normalizeModifierOnlyShortcut, normalizeShortcutSettings, shortcutToCodeMirrorKey, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
|
||||
import {
|
||||
closeOtherTabsDefaultShortcut,
|
||||
DEFAULT_SHORTCUT_SETTINGS,
|
||||
SHORTCUT_DEFINITIONS,
|
||||
findShortcutConflict,
|
||||
formatShortcut,
|
||||
normalizeModifierOnlyShortcut,
|
||||
normalizeShortcutSettings,
|
||||
selectionOccurrenceDefaultShortcut,
|
||||
shortcutToCodeMirrorKey,
|
||||
type ShortcutActionId,
|
||||
} from "@/lib/editor/shortcutRegistry";
|
||||
|
||||
describe("shortcutRegistry editor actions", () => {
|
||||
const formatterEditorActionIds: ShortcutActionId[] = [
|
||||
@@ -18,6 +29,8 @@ describe("shortcutRegistry editor actions", () => {
|
||||
"undo",
|
||||
"redo",
|
||||
"selectAll",
|
||||
"addNextSelectionOccurrence",
|
||||
"selectAllSelectionOccurrences",
|
||||
"uppercaseSelection",
|
||||
"lowercaseSelection",
|
||||
"exPasteSqlInCondition",
|
||||
@@ -233,6 +246,9 @@ describe("shortcutRegistry editor actions", () => {
|
||||
expect(shortcuts.redo).toBe("Shift+Mod+Z");
|
||||
expect(shortcuts.selectAll).toBe("Mod+A");
|
||||
expect(shortcuts.extendSelection).toBe("Alt+W");
|
||||
// 测试平台(Linux runner)解析为非 mac 默认键;mac 变体在下方单独断言。
|
||||
expect(shortcuts.addNextSelectionOccurrence).toBe(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence"));
|
||||
expect(shortcuts.selectAllSelectionOccurrences).toBe(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences"));
|
||||
expect(shortcuts.uppercaseSelection).toBe("Shift+Alt+U");
|
||||
expect(shortcuts.lowercaseSelection).toBe("Shift+Alt+L");
|
||||
expect(shortcuts.exPasteSqlInCondition).toBe("");
|
||||
@@ -246,6 +262,29 @@ describe("shortcutRegistry editor actions", () => {
|
||||
expect(DEFAULT_SHORTCUT_SETTINGS.extendSelection).toBe("Alt+W");
|
||||
});
|
||||
|
||||
it("registers occurrence selection shortcuts for query editor multi-selection", () => {
|
||||
const next = SHORTCUT_DEFINITIONS.find((item) => item.id === "addNextSelectionOccurrence");
|
||||
const all = SHORTCUT_DEFINITIONS.find((item) => item.id === "selectAllSelectionOccurrences");
|
||||
|
||||
expect(next).toMatchObject({ scope: "editor", defaultShortcut: "Ctrl+G" });
|
||||
expect(all).toMatchObject({ scope: "editor", defaultShortcut: "Ctrl+Mod+G" });
|
||||
expect(findShortcutConflict("selectAllSelectionOccurrences", DEFAULT_SHORTCUT_SETTINGS.selectAllSelectionOccurrences, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("resolves occurrence selection defaults per platform", () => {
|
||||
expect(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", "MacIntel")).toBe("Ctrl+G");
|
||||
expect(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", "MacIntel")).toBe("Ctrl+Mod+G");
|
||||
// Ctrl+Mod+G is unreachable on non-mac (CodeMirror expands Mod→Ctrl) and
|
||||
// Ctrl+G there is find-next, so Windows/Linux use the JetBrains-style keys.
|
||||
expect(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", "Win32")).toBe("Alt+J");
|
||||
expect(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", "Win32")).toBe("Ctrl+Alt+Shift+J");
|
||||
expect(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", "Linux x86_64")).toBe("Alt+J");
|
||||
expect(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", "Linux x86_64")).toBe("Ctrl+Alt+Shift+J");
|
||||
// Both platform defaults must survive the CodeMirror key conversion.
|
||||
expect(shortcutToCodeMirrorKey(selectionOccurrenceDefaultShortcut("selectAllSelectionOccurrences", "Win32"))).toBe("Ctrl-Alt-Shift-j");
|
||||
expect(shortcutToCodeMirrorKey(selectionOccurrenceDefaultShortcut("addNextSelectionOccurrence", "Win32"))).toBe("Alt-j");
|
||||
});
|
||||
|
||||
it("registers an IDEA/DataGrip-style Alt+/ shortcut for manually triggering completion", () => {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "triggerCompletion");
|
||||
|
||||
|
||||
@@ -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