diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0a9c0371a1..df9492c3d5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -38,7 +38,9 @@ import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuer import { isElectron } from "../env"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { + clampCollapsedComposerCursor, type ComposerTrigger, + collapseExpandedComposerCursor, detectComposerTrigger, expandCollapsedComposerCursor, parseStandaloneComposerSlashCommand, @@ -265,7 +267,9 @@ export default function ChatView({ threadId }: ChatViewProps) { const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< Record >({}); - const [composerCursor, setComposerCursor] = useState(() => prompt.length); + const [composerCursor, setComposerCursor] = useState(() => + collapseExpandedComposerCursor(prompt, prompt.length), + ); const [composerTrigger, setComposerTrigger] = useState(() => detectComposerTrigger(prompt, prompt.length), ); @@ -661,11 +665,12 @@ export default function ChatView({ threadId }: ChatViewProps) { } promptRef.current = nextCustomAnswer; - setComposerCursor(nextCustomAnswer.length); + const nextCursor = collapseExpandedComposerCursor(nextCustomAnswer, nextCustomAnswer.length); + setComposerCursor(nextCursor); setComposerTrigger( detectComposerTrigger( nextCustomAnswer, - expandCollapsedComposerCursor(nextCustomAnswer, nextCustomAnswer.length), + expandCollapsedComposerCursor(nextCustomAnswer, nextCursor), ), ); setComposerHighlightedItemId(null); @@ -1741,7 +1746,7 @@ export default function ChatView({ threadId }: ChatViewProps) { useEffect(() => { promptRef.current = prompt; - setComposerCursor((existing) => Math.min(Math.max(0, existing), prompt.length)); + setComposerCursor((existing) => clampCollapsedComposerCursor(prompt, existing)); }, [prompt]); useEffect(() => { @@ -1754,7 +1759,7 @@ export default function ChatView({ threadId }: ChatViewProps) { setSendPhase("idle"); setSendStartedAt(null); setComposerHighlightedItemId(null); - setComposerCursor(promptRef.current.length); + setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length)); dragDepthRef.current = 0; setIsDragOverComposer(false); @@ -2432,7 +2437,7 @@ export default function ChatView({ threadId }: ChatViewProps) { }); promptRef.current = trimmed; setPrompt(trimmed); - setComposerCursor(trimmed.length); + setComposerCursor(collapseExpandedComposerCursor(trimmed, trimmed.length)); addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry)); setComposerTrigger(detectComposerTrigger(trimmed, trimmed.length)); } @@ -2905,6 +2910,7 @@ export default function ChatView({ threadId }: ChatViewProps) { return false; } const next = replaceTextRange(promptRef.current, rangeStart, rangeEnd, replacement); + const nextCursor = collapseExpandedComposerCursor(next.text, next.cursor); promptRef.current = next.text; const activePendingQuestion = activePendingProgress?.activeQuestion; if (activePendingQuestion && activePendingUserInput) { @@ -2921,10 +2927,12 @@ export default function ChatView({ threadId }: ChatViewProps) { } else { setPrompt(next.text); } - setComposerCursor(next.cursor); - setComposerTrigger(detectComposerTrigger(next.text, next.cursor)); + setComposerCursor(nextCursor); + setComposerTrigger( + detectComposerTrigger(next.text, expandCollapsedComposerCursor(next.text, nextCursor)), + ); window.requestAnimationFrame(() => { - composerEditorRef.current?.focusAt(next.cursor); + composerEditorRef.current?.focusAt(nextCursor); }); return true; }, @@ -2934,23 +2942,37 @@ export default function ChatView({ threadId }: ChatViewProps) { const readComposerSnapshot = useCallback((): { value: string; cursor: number; + expandedCursor: number; } => { const editorSnapshot = composerEditorRef.current?.readSnapshot(); if (editorSnapshot) { return editorSnapshot; } - return { value: promptRef.current, cursor: composerCursor }; + return { + value: promptRef.current, + cursor: composerCursor, + expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), + }; }, [composerCursor]); + const extendReplacementRangeForTrailingSpace = useCallback( + (text: string, rangeEnd: number, replacement: string): number => { + if (!replacement.endsWith(" ")) { + return rangeEnd; + } + return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd; + }, + [], + ); + const resolveActiveComposerTrigger = useCallback((): { - snapshot: { value: string; cursor: number }; + snapshot: { value: string; cursor: number; expandedCursor: number }; trigger: ComposerTrigger | null; } => { const snapshot = readComposerSnapshot(); - const expandedCursor = expandCollapsedComposerCursor(snapshot.value, snapshot.cursor); return { snapshot, - trigger: detectComposerTrigger(snapshot.value, expandedCursor), + trigger: detectComposerTrigger(snapshot.value, snapshot.expandedCursor), }; }, [readComposerSnapshot]); @@ -2963,13 +2985,20 @@ export default function ChatView({ threadId }: ChatViewProps) { }); const { snapshot, trigger } = resolveActiveComposerTrigger(); if (!trigger) return; - const expectedToken = snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd); if (item.type === "path") { + const replacement = `@${item.path} `; + const replacementRangeEnd = extendReplacementRangeForTrailingSpace( + snapshot.value, + trigger.rangeEnd, + replacement, + ); const applied = applyPromptReplacement( trigger.rangeStart, - trigger.rangeEnd, - `@${item.path} `, - { expectedText: expectedToken }, + replacementRangeEnd, + replacement, + { + expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd), + }, ); if (applied) { setComposerHighlightedItemId(null); @@ -2978,9 +3007,20 @@ export default function ChatView({ threadId }: ChatViewProps) { } if (item.type === "slash-command") { if (item.command === "model") { - const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "/model ", { - expectedText: expectedToken, - }); + const replacement = "/model "; + const replacementRangeEnd = extendReplacementRangeForTrailingSpace( + snapshot.value, + trigger.rangeEnd, + replacement, + ); + const applied = applyPromptReplacement( + trigger.rangeStart, + replacementRangeEnd, + replacement, + { + expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd), + }, + ); if (applied) { setComposerHighlightedItemId(null); } @@ -2988,7 +3028,7 @@ export default function ChatView({ threadId }: ChatViewProps) { } void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { - expectedText: expectedToken, + expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), }); if (applied) { setComposerHighlightedItemId(null); @@ -2997,7 +3037,7 @@ export default function ChatView({ threadId }: ChatViewProps) { } onProviderModelSelect(item.provider, item.model); const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { - expectedText: expectedToken, + expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), }); if (applied) { setComposerHighlightedItemId(null); @@ -3005,6 +3045,7 @@ export default function ChatView({ threadId }: ChatViewProps) { }, [ applyPromptReplacement, + extendReplacementRangeForTrailingSpace, handleInteractionModeChange, onProviderModelSelect, resolveActiveComposerTrigger, @@ -3038,7 +3079,12 @@ export default function ChatView({ threadId }: ChatViewProps) { workspaceEntriesQuery.isFetching); const onPromptChange = useCallback( - (nextPrompt: string, nextCursor: number, cursorAdjacentToMention: boolean) => { + ( + nextPrompt: string, + nextCursor: number, + nextExpandedCursor: number, + cursorAdjacentToMention: boolean, + ) => { if (activePendingProgress?.activeQuestion && activePendingUserInput) { onChangeActivePendingUserInputCustomAnswer( activePendingProgress.activeQuestion.id, @@ -3052,12 +3098,7 @@ export default function ChatView({ threadId }: ChatViewProps) { setPrompt(nextPrompt); setComposerCursor(nextCursor); setComposerTrigger( - cursorAdjacentToMention - ? null - : detectComposerTrigger( - nextPrompt, - expandCollapsedComposerCursor(nextPrompt, nextCursor), - ), + cursorAdjacentToMention ? null : detectComposerTrigger(nextPrompt, nextExpandedCursor), ); }, [ diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index f9321de651..3d6d35a9bb 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -47,7 +47,12 @@ import { type Ref, } from "react"; -import { isCollapsedCursorAdjacentToMention } from "~/composer-logic"; +import { + clampCollapsedComposerCursor, + collapseExpandedComposerCursor, + expandCollapsedComposerCursor, + isCollapsedCursorAdjacentToMention, +} from "~/composer-logic"; import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions"; import { cn } from "~/lib/utils"; import { basenameOfPath, getVscodeIconUrlForEntry } from "~/vscode-icons"; @@ -171,14 +176,14 @@ function renderMentionChipDom(container: HTMLElement, pathValue: string): void { container.append(icon, label); } -function clampCursor(value: string, cursor: number): number { +function clampExpandedCursor(value: string, cursor: number): number { if (!Number.isFinite(cursor)) return value.length; return Math.max(0, Math.min(value.length, Math.floor(cursor))); } -function getComposerNodeTextLength(node: LexicalNode): number { +function getComposerNodeTextLength(node: LexicalNode, mode: "collapsed" | "expanded"): number { if (node instanceof ComposerMentionNode) { - return 1; + return mode === "collapsed" ? 1 : node.getTextContentSize(); } if ($isTextNode(node)) { return node.getTextContentSize(); @@ -187,12 +192,18 @@ function getComposerNodeTextLength(node: LexicalNode): number { return 1; } if ($isElementNode(node)) { - return node.getChildren().reduce((total, child) => total + getComposerNodeTextLength(child), 0); + return node + .getChildren() + .reduce((total, child) => total + getComposerNodeTextLength(child, mode), 0); } return 0; } -function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number { +function getAbsoluteOffsetForPoint( + node: LexicalNode, + pointOffset: number, + mode: "collapsed" | "expanded", +): number { let offset = 0; let current: LexicalNode | null = node; @@ -206,14 +217,14 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb for (let i = 0; i < index; i += 1) { const sibling = siblings[i]; if (!sibling) continue; - offset += getComposerNodeTextLength(sibling); + offset += getComposerNodeTextLength(sibling, mode); } current = nextParent; } if ($isTextNode(node)) { if (node instanceof ComposerMentionNode) { - return offset + (pointOffset > 0 ? 1 : 0); + return offset + (pointOffset > 0 ? getComposerNodeTextLength(node, mode) : 0); } return offset + Math.min(pointOffset, node.getTextContentSize()); } @@ -228,7 +239,7 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb for (let i = 0; i < clampedOffset; i += 1) { const child = children[i]; if (!child) continue; - offset += getComposerNodeTextLength(child); + offset += getComposerNodeTextLength(child, mode); } return offset; } @@ -320,7 +331,7 @@ function findSelectionPointAtOffset( function $getComposerRootLength(): number { const root = $getRoot(); const children = root.getChildren(); - return children.reduce((sum, child) => sum + getComposerNodeTextLength(child), 0); + return children.reduce((sum, child) => sum + getComposerNodeTextLength(child, "collapsed"), 0); } function $setSelectionAtComposerOffset(nextOffset: number): void { @@ -345,11 +356,22 @@ function $readSelectionOffsetFromEditorState(fallback: number): number { return fallback; } const anchorNode = selection.anchor.getNode(); - const offset = getAbsoluteOffsetForPoint(anchorNode, selection.anchor.offset); + const offset = getAbsoluteOffsetForPoint(anchorNode, selection.anchor.offset, "collapsed"); const composerLength = $getComposerRootLength(); return Math.max(0, Math.min(offset, composerLength)); } +function $readExpandedSelectionOffsetFromEditorState(fallback: number): number { + const selection = $getSelection(); + if (!$isRangeSelection(selection) || !selection.isCollapsed()) { + return fallback; + } + const anchorNode = selection.anchor.getNode(); + const offset = getAbsoluteOffsetForPoint(anchorNode, selection.anchor.offset, "expanded"); + const expandedLength = $getRoot().getTextContent().length; + return Math.max(0, Math.min(offset, expandedLength)); +} + function $appendTextWithLineBreaks(parent: ElementNode, text: string): void { const lines = text.split("\n"); for (let index = 0; index < lines.length; index += 1) { @@ -383,7 +405,7 @@ export interface ComposerPromptEditorHandle { focus: () => void; focusAt: (cursor: number) => void; focusAtEnd: () => void; - readSnapshot: () => { value: string; cursor: number }; + readSnapshot: () => { value: string; cursor: number; expandedCursor: number }; } interface ComposerPromptEditorProps { @@ -392,7 +414,12 @@ interface ComposerPromptEditorProps { disabled: boolean; placeholder: string; className?: string; - onChange: (nextValue: string, nextCursor: number, cursorAdjacentToMention: boolean) => void; + onChange: ( + nextValue: string, + nextCursor: number, + nextExpandedCursor: number, + cursorAdjacentToMention: boolean, + ) => void; onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent, @@ -538,7 +565,7 @@ function ComposerMentionSelectionNormalizePlugin() { const anchorNode = selection.anchor.getNode(); if (!(anchorNode instanceof ComposerMentionNode)) return; if (selection.anchor.offset === 0) return; - const beforeOffset = getAbsoluteOffsetForPoint(anchorNode, 0); + const beforeOffset = getAbsoluteOffsetForPoint(anchorNode, 0, "collapsed"); afterOffset = beforeOffset + 1; }); if (afterOffset !== null) { @@ -571,7 +598,7 @@ function ComposerMentionBackspacePlugin() { if (!(candidate instanceof ComposerMentionNode)) { return false; } - const mentionStart = getAbsoluteOffsetForPoint(candidate, 0); + const mentionStart = getAbsoluteOffsetForPoint(candidate, 0, "collapsed"); candidate.remove(); $setSelectionAtComposerOffset(mentionStart); event?.preventDefault(); @@ -628,7 +655,12 @@ function ComposerPromptEditorInner({ }: ComposerPromptEditorInnerProps) { const [editor] = useLexicalComposerContext(); const onChangeRef = useRef(onChange); - const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) }); + const initialCursor = clampCollapsedComposerCursor(value, cursor); + const snapshotRef = useRef({ + value, + cursor: initialCursor, + expandedCursor: expandCollapsedComposerCursor(value, initialCursor), + }); const isApplyingControlledUpdateRef = useRef(false); useEffect(() => { @@ -640,13 +672,17 @@ function ComposerPromptEditorInner({ }, [disabled, editor]); useLayoutEffect(() => { - const normalizedCursor = clampCursor(value, cursor); + const normalizedCursor = clampCollapsedComposerCursor(value, cursor); const previousSnapshot = snapshotRef.current; if (previousSnapshot.value === value && previousSnapshot.cursor === normalizedCursor) { return; } - snapshotRef.current = { value, cursor: normalizedCursor }; + snapshotRef.current = { + value, + cursor: normalizedCursor, + expandedCursor: expandCollapsedComposerCursor(value, normalizedCursor), + }; const rootElement = editor.getRootElement(); const isFocused = Boolean(rootElement && document.activeElement === rootElement); @@ -673,7 +709,7 @@ function ComposerPromptEditorInner({ (nextCursor: number) => { const rootElement = editor.getRootElement(); if (!rootElement) return; - const boundedCursor = clampCursor(snapshotRef.current.value, nextCursor); + const boundedCursor = clampCollapsedComposerCursor(snapshotRef.current.value, nextCursor); rootElement.focus(); editor.update(() => { $setSelectionAtComposerOffset(boundedCursor); @@ -681,24 +717,43 @@ function ComposerPromptEditorInner({ snapshotRef.current = { value: snapshotRef.current.value, cursor: boundedCursor, + expandedCursor: expandCollapsedComposerCursor(snapshotRef.current.value, boundedCursor), }; - onChangeRef.current(snapshotRef.current.value, boundedCursor, false); + onChangeRef.current( + snapshotRef.current.value, + boundedCursor, + snapshotRef.current.expandedCursor, + false, + ); }, [editor], ); - const readSnapshot = useCallback((): { value: string; cursor: number } => { + const readSnapshot = useCallback((): { + value: string; + cursor: number; + expandedCursor: number; + } => { let snapshot = snapshotRef.current; editor.getEditorState().read(() => { const nextValue = $getRoot().getTextContent(); - const fallbackCursor = clampCursor(nextValue, snapshotRef.current.cursor); - const nextCursor = clampCursor( + const fallbackCursor = clampCollapsedComposerCursor(nextValue, snapshotRef.current.cursor); + const nextCursor = clampCollapsedComposerCursor( nextValue, $readSelectionOffsetFromEditorState(fallbackCursor), ); + const fallbackExpandedCursor = clampExpandedCursor( + nextValue, + snapshotRef.current.expandedCursor, + ); + const nextExpandedCursor = clampExpandedCursor( + nextValue, + $readExpandedSelectionOffsetFromEditorState(fallbackExpandedCursor), + ); snapshot = { value: nextValue, cursor: nextCursor, + expandedCursor: nextExpandedCursor, }; }); snapshotRef.current = snapshot; @@ -713,7 +768,12 @@ function ComposerPromptEditorInner({ }, focusAt, focusAtEnd: () => { - focusAt(snapshotRef.current.value.length); + focusAt( + collapseExpandedComposerCursor( + snapshotRef.current.value, + snapshotRef.current.value.length, + ), + ); }, readSnapshot, }), @@ -723,13 +783,25 @@ function ComposerPromptEditorInner({ const handleEditorChange = useCallback((editorState: EditorState) => { editorState.read(() => { const nextValue = $getRoot().getTextContent(); - const fallbackCursor = clampCursor(nextValue, snapshotRef.current.cursor); - const nextCursor = clampCursor( + const fallbackCursor = clampCollapsedComposerCursor(nextValue, snapshotRef.current.cursor); + const nextCursor = clampCollapsedComposerCursor( nextValue, $readSelectionOffsetFromEditorState(fallbackCursor), ); + const fallbackExpandedCursor = clampExpandedCursor( + nextValue, + snapshotRef.current.expandedCursor, + ); + const nextExpandedCursor = clampExpandedCursor( + nextValue, + $readExpandedSelectionOffsetFromEditorState(fallbackExpandedCursor), + ); const previousSnapshot = snapshotRef.current; - if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) { + if ( + previousSnapshot.value === nextValue && + previousSnapshot.cursor === nextCursor && + previousSnapshot.expandedCursor === nextExpandedCursor + ) { return; } if (isApplyingControlledUpdateRef.current) { @@ -738,11 +810,12 @@ function ComposerPromptEditorInner({ snapshotRef.current = { value: nextValue, cursor: nextCursor, + expandedCursor: nextExpandedCursor, }; const cursorAdjacentToMention = isCollapsedCursorAdjacentToMention(nextValue, nextCursor, "left") || isCollapsedCursorAdjacentToMention(nextValue, nextCursor, "right"); - onChangeRef.current(nextValue, nextCursor, cursorAdjacentToMention); + onChangeRef.current(nextValue, nextCursor, nextExpandedCursor, cursorAdjacentToMention); }); }, []); diff --git a/apps/web/src/composer-logic.test.ts b/apps/web/src/composer-logic.test.ts index 7e6805c96d..371fab9677 100644 --- a/apps/web/src/composer-logic.test.ts +++ b/apps/web/src/composer-logic.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { + clampCollapsedComposerCursor, + collapseExpandedComposerCursor, detectComposerTrigger, expandCollapsedComposerCursor, isCollapsedCursorAdjacentToMention, @@ -92,6 +94,44 @@ describe("expandCollapsedComposerCursor", () => { }); }); +describe("collapseExpandedComposerCursor", () => { + it("keeps cursor unchanged when no mention segment is present", () => { + expect(collapseExpandedComposerCursor("plain text", 5)).toBe(5); + }); + + it("maps expanded mention cursor back to collapsed cursor", () => { + const text = "what's in my @AGENTS.md fsfdas"; + const collapsedCursorAfterMention = "what's in my ".length + 2; + const expandedCursorAfterMention = "what's in my @AGENTS.md ".length; + + expect(collapseExpandedComposerCursor(text, expandedCursorAfterMention)).toBe( + collapsedCursorAfterMention, + ); + }); + + it("keeps replacement cursors aligned when another mention already exists earlier", () => { + const text = "open @AGENTS.md then @src/index.ts "; + const expandedCursor = text.length; + const collapsedCursor = collapseExpandedComposerCursor(text, expandedCursor); + + expect(collapsedCursor).toBe("open ".length + 1 + " then ".length + 2); + expect(expandCollapsedComposerCursor(text, collapsedCursor)).toBe(expandedCursor); + }); +}); + +describe("clampCollapsedComposerCursor", () => { + it("clamps to collapsed prompt length when mentions are present", () => { + const text = "open @AGENTS.md then "; + + expect(clampCollapsedComposerCursor(text, text.length)).toBe( + "open ".length + 1 + " then ".length, + ); + expect(clampCollapsedComposerCursor(text, Number.POSITIVE_INFINITY)).toBe( + "open ".length + 1 + " then ".length, + ); + }); +}); + describe("isCollapsedCursorAdjacentToMention", () => { it("returns false when no mention exists", () => { expect(isCollapsedCursorAdjacentToMention("plain text", 6, "left")).toBe(false); diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index 70b3567c3f..b696d80381 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -67,7 +67,7 @@ function collapsedSegmentLength( return segment.type === "mention" ? 1 : segment.text.length; } -function clampCollapsedComposerCursor( +function clampCollapsedComposerCursorForSegments( segments: ReadonlyArray<{ type: "text"; text: string } | { type: "mention" }>, cursorInput: number, ): number { @@ -81,6 +81,48 @@ function clampCollapsedComposerCursor( return Math.max(0, Math.min(collapsedLength, Math.floor(cursorInput))); } +export function clampCollapsedComposerCursor(text: string, cursorInput: number): number { + return clampCollapsedComposerCursorForSegments( + splitPromptIntoComposerSegments(text), + cursorInput, + ); +} + +export function collapseExpandedComposerCursor(text: string, cursorInput: number): number { + const expandedCursor = clampCursor(text, cursorInput); + const segments = splitPromptIntoComposerSegments(text); + if (segments.length === 0) { + return expandedCursor; + } + + let remaining = expandedCursor; + let collapsedCursor = 0; + + for (const segment of segments) { + if (segment.type === "mention") { + const expandedLength = segment.path.length + 1; + if (remaining === 0) { + return collapsedCursor; + } + if (remaining <= expandedLength) { + return collapsedCursor + 1; + } + remaining -= expandedLength; + collapsedCursor += 1; + continue; + } + + const segmentLength = segment.text.length; + if (remaining <= segmentLength) { + return collapsedCursor + remaining; + } + remaining -= segmentLength; + collapsedCursor += segmentLength; + } + + return collapsedCursor; +} + export function isCollapsedCursorAdjacentToMention( text: string, cursorInput: number, @@ -91,7 +133,7 @@ export function isCollapsedCursorAdjacentToMention( return false; } - const cursor = clampCollapsedComposerCursor(segments, cursorInput); + const cursor = clampCollapsedComposerCursorForSegments(segments, cursorInput); let collapsedOffset = 0; for (const segment of segments) {