From 299fb14f807dda78332ad52a4568354ad0e098a0 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:57:07 +0800 Subject: [PATCH] fix(editor): hide mouse-only caret action from Find Action (#2191) Add or Remove Caret only works through its pointer gesture, so running it from Find Action did nothing. It is now hidden there. Hidden actions with default shortcuts or explicit overrides stay searchable and editable in Settings > Keymap, including after their last binding is removed. A sweep of other empty handlers found only disabled popup placeholders and an initially disabled merge editor host. --- src/editor/core/actions.ts | 1 + .../settings/__tests__/keymap.test.ts | 25 ++++++++++++++++++- src/features/settings/keymap/KeymapPage.tsx | 8 +++--- src/features/settings/keymap/model.ts | 5 ++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/editor/core/actions.ts b/src/editor/core/actions.ts index fb82ead1b..0288c81a1 100644 --- a/src/editor/core/actions.ts +++ b/src/editor/core/actions.ts @@ -806,6 +806,7 @@ export function registerEditorActions(): () => void { title: "Add or Remove Caret", category: "Editor Actions", when: "textEditorActive", + hidden: true, // The mouse selection style handles the pointer position and dragging. run: () => {}, }, diff --git a/src/features/settings/__tests__/keymap.test.ts b/src/features/settings/__tests__/keymap.test.ts index 470a34988..89d5ff6da 100644 --- a/src/features/settings/__tests__/keymap.test.ts +++ b/src/features/settings/__tests__/keymap.test.ts @@ -36,9 +36,11 @@ import { withCustomOverrides, type KeymapDraft, } from "../keymap/model"; -import { buildKeymapTree } from "../keymap/KeymapPage"; +import { buildKeymapTree, keymapSearchLabels } from "../keymap/KeymapPage"; import { groupPath } from "../keymap/groups"; +import { registerEditorActions } from "@/editor/core/actions"; import { registerLspActions } from "@/editor/lsp/actions"; +import { matchActions } from "@/features/search/everywhere/model"; import { _resetI18nForTests, setLocale } from "@/platform/i18n"; it("finds the translated declaration action by 声明 and its English title", async () => { @@ -204,6 +206,27 @@ describe("conflicts", () => { }); describe("shortcut search", () => { + it.each(Object.keys(PRESETS))("keeps the mouse-only caret action configurable in %s", (preset) => { + const dispose = registerEditorActions(); + const originalKeymap = useKeymap.getState(); + const id = "editor.addOrRemoveCaret"; + const title = "Add or Remove Caret"; + try { + expect(matchActions(allActions(), title, { isEnabled: () => true, recent: [id] })).toEqual([]); + const variants: KeymapDraft["overrides"][] = [{}, { [id]: ["Alt+Button1"] }, { [id]: [] }]; + for (const overrides of variants) { + useKeymap.setState({ preset, overrides }); + expect(keymapSearchLabels()).toContain(title); + const tree = buildKeymapTree(allActions(), { preset, overrides }, title, ""); + const group = tree.find((node) => node.key === "grp:Editor Actions"); + expect(group?.kind === "group" && group.children.map((node) => node.key)).toEqual([id]); + } + } finally { + useKeymap.setState(originalKeymap); + dispose(); + } + }); + it("matches exact bindings and chord first strokes", () => { expect(bindingMatches(["Meta+K Meta+C"], "Meta+K")).toBe(true); expect(bindingMatches(["Shift+Meta+O"], "Meta+Shift+O")).toBe(true); diff --git a/src/features/settings/keymap/KeymapPage.tsx b/src/features/settings/keymap/KeymapPage.tsx index f97ad6654..abd325a50 100644 --- a/src/features/settings/keymap/KeymapPage.tsx +++ b/src/features/settings/keymap/KeymapPage.tsx @@ -9,7 +9,7 @@ import { useMemo, useRef, useState, type MouseEvent } from "react"; import clsx from "clsx"; import { useActions, allActions, type Action } from "@/platform/actions"; -import { defaultPreset, formatBinding, type Keymap, PRESET_TITLES, PRESETS } from "@/platform/keymap"; +import { defaultPreset, formatBinding, type Keymap, PRESET_TITLES, PRESETS, useKeymap } from "@/platform/keymap"; import { confirm } from "@/workbench/messages"; import { HighlightedText, @@ -36,6 +36,7 @@ import { deleteKeymap, duplicateKeymap, isModified, + isKeymapAction, keymapName, removeBinding, removeConflicts, @@ -61,8 +62,9 @@ type KNode = type GroupNode = Extract; export function keymapSearchLabels(): string[] { + const draft = useKeymap.getState(); return allActions() - .filter((a) => !a.hidden) + .filter((a) => isKeymapAction(a, draft)) .map((a) => a.title); } @@ -107,7 +109,7 @@ export function buildKeymapTree( const q = query.toLowerCase(); for (const action of actions) { const bindings = resolveBindings(draft, action.id); - if (action.hidden && !bindings.length) continue; + if (!isKeymapAction(action, draft)) continue; let hits: number[] | null = null; if (query) { hits = wordMatch(action.title, query); diff --git a/src/features/settings/keymap/model.ts b/src/features/settings/keymap/model.ts index e5d335b24..ed7097420 100644 --- a/src/features/settings/keymap/model.ts +++ b/src/features/settings/keymap/model.ts @@ -27,6 +27,11 @@ export function resolveBindings(draft: KeymapDraft, id: string): string[] { return draft.overrides[id]?.map(normalizeBinding) ?? presetBindings(draft.preset, id); } +/** Hidden actions with shortcuts stay configurable even after their bindings are removed. */ +export function isKeymapAction(action: Action, draft: KeymapDraft): boolean { + return !action.hidden || action.id in draft.overrides || presetBindings(draft.preset, action.id).length > 0; +} + export function isModified(draft: KeymapDraft, id: string): boolean { return id in draft.overrides; }