Skip to content

Limit composer arrow-key mention jumps to adjacent mention boundaries#169

Merged
juliusmarminge merged 2 commits intomainfrom
t3code/fix-composer-pill-arrow-overrides
Mar 5, 2026
Merged

Limit composer arrow-key mention jumps to adjacent mention boundaries#169
juliusmarminge merged 2 commits intomainfrom
t3code/fix-composer-pill-arrow-overrides

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 5, 2026

Summary

  • Restricts custom left/right arrow handling in ComposerPromptEditor so it only overrides navigation when the collapsed cursor is directly adjacent to a mention boundary.
  • Adds isCollapsedCursorAdjacentToMention to composer logic, including cursor clamping across collapsed mention/text segments.
  • Prevents default/propagation only when an actual mention-boundary jump is performed.
  • Adds unit tests covering non-mention text and precise left/right adjacency behavior.

Testing

  • apps/web/src/composer-logic.test.ts: added tests for isCollapsedCursorAdjacentToMention (no mention, left adjacency, right adjacency).
  • Not run: bun lint
  • Not run: bun typecheck
  • Not run: full test suite

Note

Medium Risk
Changes keyboard navigation and cursor-adjacency detection around mention pills, which is UX-critical and can regress selection behavior in edge cases. Scope is contained to composer editor logic with added unit coverage.

Overview
Arrow-key handling in the composer editor is now gated by mention-boundary adjacency. ComposerMentionArrowPlugin only prevents default left/right navigation and performs the custom offset jump when the collapsed cursor is directly adjacent to a mention (otherwise Lexical handles the key normally).

Adds isCollapsedCursorAdjacentToMention in composer-logic.ts (with cursor clamping over collapsed mention/text segments) and updates ComposerPromptEditor’s onChange callback to compute cursorAdjacentToMention via this shared logic. Includes new unit tests for adjacency behavior in composer-logic.test.ts.

Written by Cursor Bugbot for commit 05dbf05. This will update automatically on new commits. Configure here.

Note

Limit left/right arrow handling in the Composer prompt editor to move the cursor only when adjacent to a mention to constrain mention boundary jumps

Update ComposerMentionArrowPlugin to intercept left/right keys only when isCollapsedCursorAdjacentToMention detects adjacency, adjust selection by ±1, and prevent default; add isCollapsedCursorAdjacentToMention with collapsed-segment logic and tests; update the onChange pipeline to pass adjacency based on text-derived checks. See ComposerPromptEditor.tsx, composer-logic.ts, and composer-logic.test.ts.

📍Where to Start

Start with the left/right key handlers in ComposerMentionArrowPlugin in ComposerPromptEditor.tsx, then review isCollapsedCursorAdjacentToMention in composer-logic.ts and its tests in composer-logic.test.ts.

Macroscope summarized 05dbf05.

- Only intercept left/right arrow keys when cursor is directly next to a mention
- Prevent default arrow behavior only for those mention-boundary moves
- Add `isCollapsedCursorAdjacentToMention` logic with coverage for left/right adjacency cases
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 5, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a8913bfa-3ff4-481a-968c-24f824e22f4b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/fix-composer-pill-arrow-overrides

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +473 to 475
(event) => {
let nextOffset: number | null = null;
editor.getEditorState().read(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/ComposerPromptEditor.tsx:473

When the user presses Shift + ArrowLeft or Shift + ArrowRight next to a mention, the ComposerMentionArrowPlugin consumes the event and moves the cursor instead of allowing Lexical to extend the selection. This makes mention nodes impossible to select via keyboard. Consider adding a check for event?.shiftKey and returning false early when the shift modifier is held, so standard selection behavior is preserved.

      (event) => {
+        if (event?.shiftKey) return false;
        let nextOffset: number | null = null;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ComposerPromptEditor.tsx around lines 473-475:

When the user presses `Shift + ArrowLeft` or `Shift + ArrowRight` next to a mention, the `ComposerMentionArrowPlugin` consumes the event and moves the cursor instead of allowing Lexical to extend the selection. This makes mention nodes impossible to select via keyboard. Consider adding a check for `event?.shiftKey` and returning `false` early when the shift modifier is held, so standard selection behavior is preserved.

Evidence trail:
apps/web/src/components/ComposerPromptEditor.tsx lines 472-519 at REVIEWED_COMMIT. The KEY_ARROW_LEFT_COMMAND handler (lines 472-495) and KEY_ARROW_RIGHT_COMMAND handler (lines 496-519) both check `!selection.isCollapsed()` but neither checks `event?.shiftKey`. When adjacent to a mention with collapsed selection, both handlers call `event?.preventDefault()`, `event?.stopPropagation()`, and return `true`, consuming the event regardless of whether Shift is held.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Regex-based mention detection mismatches Lexical node tree
    • Replaced the regex-based isCollapsedCursorAdjacentToMention (which required trailing whitespace and operated in raw-text coordinates) with a Lexical node tree-based $isCursorAdjacentToMentionInDirection that inspects adjacent ComposerMentionNode siblings directly, eliminating both the regex detection gap and the coordinate system mismatch.

Create PR

Or push these changes by commenting:

@cursor push 083714b064
Preview (083714b064)
diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx
--- a/apps/web/src/components/ComposerPromptEditor.tsx
+++ b/apps/web/src/components/ComposerPromptEditor.tsx
@@ -50,7 +50,6 @@
   type Ref,
 } from "react";
 
-import { isCollapsedCursorAdjacentToMention } from "~/composer-logic";
 import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions";
 import { cn } from "~/lib/utils";
 import { basenameOfPath, getVscodeIconUrlForEntry } from "~/vscode-icons";
@@ -477,10 +476,7 @@
           if (!$isRangeSelection(selection) || !selection.isCollapsed()) return;
           const currentOffset = $readSelectionOffsetFromEditorState(0);
           if (currentOffset <= 0) return;
-          const promptValue = $getRoot().getTextContent();
-          if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "left")) {
-            return;
-          }
+          if (!$isCursorAdjacentToMentionInDirection("left")) return;
           nextOffset = currentOffset - 1;
         });
         if (nextOffset === null) return false;
@@ -504,10 +500,7 @@
           const currentOffset = $readSelectionOffsetFromEditorState(0);
           const composerLength = $getComposerRootLength();
           if (currentOffset >= composerLength) return;
-          const promptValue = $getRoot().getTextContent();
-          if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "right")) {
-            return;
-          }
+          if (!$isCursorAdjacentToMentionInDirection("right")) return;
           nextOffset = currentOffset + 1;
         });
         if (nextOffset === null) return false;
@@ -559,25 +552,40 @@
 }
 
 function $isCursorAdjacentToMention(): boolean {
+  return $isCursorAdjacentToMentionInDirection("left");
+}
+
+function $isCursorAdjacentToMentionInDirection(direction: "left" | "right"): boolean {
   const selection = $getSelection();
   if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false;
   const anchorNode = selection.anchor.getNode();
+  const anchorOffset = selection.anchor.offset;
 
   if (anchorNode instanceof ComposerMentionNode) return true;
 
-  if ($isElementNode(anchorNode)) {
-    const childIndex = selection.anchor.offset - 1;
-    if (childIndex >= 0) {
-      const child = anchorNode.getChildAtIndex(childIndex);
+  if (direction === "left") {
+    if ($isElementNode(anchorNode)) {
+      const childIndex = anchorOffset - 1;
+      if (childIndex >= 0) {
+        const child = anchorNode.getChildAtIndex(childIndex);
+        if (child instanceof ComposerMentionNode) return true;
+      }
+    }
+    if ($isTextNode(anchorNode) && anchorOffset === 0) {
+      const prev = anchorNode.getPreviousSibling();
+      if (prev instanceof ComposerMentionNode) return true;
+    }
+  } else {
+    if ($isElementNode(anchorNode)) {
+      const child = anchorNode.getChildAtIndex(anchorOffset);
       if (child instanceof ComposerMentionNode) return true;
     }
+    if ($isTextNode(anchorNode) && anchorOffset === anchorNode.getTextContentSize()) {
+      const next = anchorNode.getNextSibling();
+      if (next instanceof ComposerMentionNode) return true;
+    }
   }
 
-  if ($isTextNode(anchorNode) && selection.anchor.offset === 0) {
-    const prev = anchorNode.getPreviousSibling();
-    if (prev instanceof ComposerMentionNode) return true;
-  }
-
   return false;
 }

}

return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex-based mention detection mismatches Lexical node tree

Medium Severity

isCollapsedCursorAdjacentToMention re-detects mentions from raw text using splitPromptIntoComposerSegments, whose regex requires trailing whitespace ((?=\s)). But the Lexical editor identifies mentions by node type (ComposerMentionNode). When a mention lacks trailing whitespace (e.g., at end of text, or user deleted the space), the regex won't find it, so the collapsed cursor offset from Lexical (where mention = 1 unit) gets interpreted in a mismatched coordinate system (where the full @path text counts at its actual length). The arrow key override silently fails to activate. The old code avoided this by always jumping in Lexical's collapsed space without relying on regex re-detection.

Additional Locations (2)

Fix in Cursor Fix in Web

- replace Lexical-node cursor adjacency check with text/cursor helper
- prevent `@query` typing from being treated as mention-pill adjacent
- add regression test for non-pill `@pac` input
@juliusmarminge juliusmarminge merged commit a2682de into main Mar 5, 2026
4 checks passed
@juliusmarminge juliusmarminge deleted the t3code/fix-composer-pill-arrow-overrides branch March 5, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant