import { LexicalComposer, type InitialConfigType,} from "@lexical/react/LexicalComposer";import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";import { ContentEditable } from "@lexical/react/LexicalContentEditable";import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";import { $applyNodeReplacement, $createRangeSelection, $getSelection, $setSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, $createLineBreakNode, $createParagraphNode, $createTextNode, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, COMMAND_PRIORITY_HIGH, KEY_BACKSPACE_COMMAND, $getRoot, type ElementNode, type LexicalNode, TextNode, type EditorConfig, type EditorState, type NodeKey, type SerializedTextNode, type Spread,} from "lexical";import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, type ClipboardEventHandler, 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"; const COMPOSER_EDITOR_HMR_KEY = `composer-editor-${Math.random().toString(36).slice(2)}`; type SerializedComposerMentionNode = Spread< { path: string; type: "composer-mention"; version: 1; }, SerializedTextNode>; class ComposerMentionNode extends TextNode { __path: string; static override getType(): string { return "composer-mention"; } static override clone(node: ComposerMentionNode): ComposerMentionNode { return new ComposerMentionNode(node.__path, node.__key); } static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode { return $createComposerMentionNode(serializedNode.path); } constructor(path: string, key?: NodeKey) { const normalizedPath = path.startsWith("@") ? path.slice(1) : path; super(`@${normalizedPath}`, key); this.__path = normalizedPath; } override exportJSON(): SerializedComposerMentionNode { return { ...super.exportJSON(), path: this.__path, type: "composer-mention", version: 1, }; } override createDOM(_config: EditorConfig): HTMLElement { const dom = document.createElement("span"); dom.className = "inline-flex select-none items-center gap-1 rounded-md border border-border/70 bg-accent/40 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-foreground align-middle"; dom.contentEditable = "false"; dom.setAttribute("spellcheck", "false"); renderMentionChipDom(dom, this.__path); return dom; } override updateDOM( prevNode: ComposerMentionNode, dom: HTMLElement, _config: EditorConfig, ): boolean { dom.contentEditable = "false"; if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) { renderMentionChipDom(dom, this.__path); } return false; } override canInsertTextBefore(): false { return false; } override canInsertTextAfter(): false { return false; } override isTextEntity(): true { return true; } override isToken(): true { return true; }} function $createComposerMentionNode(path: string): ComposerMentionNode { return $applyNodeReplacement(new ComposerMentionNode(path));} function inferMentionPathKind(pathValue: string): "file" | "directory" { const base = basenameOfPath(pathValue); if (base.startsWith(".") && !base.slice(1).includes(".")) { return "directory"; } if (base.includes(".")) { return "file"; } return "directory";} function resolvedThemeFromDocument(): "light" | "dark" { return document.documentElement.classList.contains("dark") ? "dark" : "light";} function renderMentionChipDom(container: HTMLElement, pathValue: string): void { container.textContent = ""; container.style.setProperty("user-select", "none"); container.style.setProperty("-webkit-user-select", "none"); const theme = resolvedThemeFromDocument(); const icon = document.createElement("img"); icon.alt = ""; icon.ariaHidden = "true"; icon.className = "size-3.5 shrink-0 opacity-85"; icon.loading = "lazy"; icon.src = getVscodeIconUrlForEntry(pathValue, inferMentionPathKind(pathValue), theme); const label = document.createElement("span"); label.className = "truncate select-none leading-tight"; label.textContent = basenameOfPath(pathValue); container.append(icon, label);} function clampCursor(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 { if (node instanceof ComposerMentionNode) { return 1; } if ($isTextNode(node)) { return node.getTextContentSize(); } if ($isLineBreakNode(node)) { return 1; } if ($isElementNode(node)) { return node.getChildren().reduce((total, child) => total + getComposerNodeTextLength(child), 0); } return 0;} function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number { let offset = 0; let current: LexicalNode | null = node; while (current) { const nextParent = current.getParent() as LexicalNode | null; if (!nextParent || !$isElementNode(nextParent)) { break; } const siblings = nextParent.getChildren(); const index = current.getIndexWithinParent(); for (let i = 0; i < index; i += 1) { const sibling = siblings[i]; if (!sibling) continue; offset += getComposerNodeTextLength(sibling); } current = nextParent; } if ($isTextNode(node)) { if (node instanceof ComposerMentionNode) { return offset + (pointOffset > 0 ? 1 : 0); } return offset + Math.min(pointOffset, node.getTextContentSize()); } if ($isLineBreakNode(node)) { return offset + Math.min(pointOffset, 1); } if ($isElementNode(node)) { const children = node.getChildren(); const clampedOffset = Math.max(0, Math.min(pointOffset, children.length)); for (let i = 0; i < clampedOffset; i += 1) { const child = children[i]; if (!child) continue; offset += getComposerNodeTextLength(child); } return offset; } return offset;} function findSelectionPointAtOffset( node: LexicalNode, remainingRef: { value: number },): { key: string; offset: number; type: "text" | "element" } | null { if (node instanceof ComposerMentionNode) { const parent = node.getParent(); if (!parent || !$isElementNode(parent)) return null; const index = node.getIndexWithinParent(); if (remainingRef.value === 0) { return { key: parent.getKey(), offset: index, type: "element", }; } if (remainingRef.value === 1) { return { key: parent.getKey(), offset: index + 1, type: "element", }; } remainingRef.value -= 1; return null; } if ($isTextNode(node)) { const size = node.getTextContentSize(); if (remainingRef.value <= size) { return { key: node.getKey(), offset: remainingRef.value, type: "text", }; } remainingRef.value -= size; return null; } if ($isLineBreakNode(node)) { const parent = node.getParent(); if (!parent) return null; const index = node.getIndexWithinParent(); if (remainingRef.value === 0) { return { key: parent.getKey(), offset: index, type: "element", }; } if (remainingRef.value === 1) { return { key: parent.getKey(), offset: index + 1, type: "element", }; } remainingRef.value -= 1; return null; } if ($isElementNode(node)) { const children = node.getChildren(); for (const child of children) { const point = findSelectionPointAtOffset(child, remainingRef); if (point) { return point; } } if (remainingRef.value === 0) { return { key: node.getKey(), offset: children.length, type: "element", }; } } return null;} function $getComposerRootLength(): number { const root = $getRoot(); const children = root.getChildren(); return children.reduce((sum, child) => sum + getComposerNodeTextLength(child), 0);} function $setSelectionAtComposerOffset(nextOffset: number): void { const root = $getRoot(); const composerLength = $getComposerRootLength(); const boundedOffset = Math.max(0, Math.min(nextOffset, composerLength)); const remainingRef = { value: boundedOffset }; const point = findSelectionPointAtOffset(root, remainingRef) ?? { key: root.getKey(), offset: root.getChildren().length, type: "element" as const, }; const selection = $createRangeSelection(); selection.anchor.set(point.key, point.offset, point.type); selection.focus.set(point.key, point.offset, point.type); $setSelection(selection);} function $readSelectionOffsetFromEditorState(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); const composerLength = $getComposerRootLength(); return Math.max(0, Math.min(offset, composerLength));} function $appendTextWithLineBreaks(parent: ElementNode, text: string): void { const lines = text.split("\n"); for (let index = 0; index < lines.length; index += 1) { const line = lines[index] ?? ""; if (line.length > 0) { parent.append($createTextNode(line)); } if (index < lines.length - 1) { parent.append($createLineBreakNode()); } }} function $setComposerEditorPrompt(prompt: string): void { const root = $getRoot(); root.clear(); const paragraph = $createParagraphNode(); root.append(paragraph); const segments = splitPromptIntoComposerSegments(prompt); for (const segment of segments) { if (segment.type === "mention") { paragraph.append($createComposerMentionNode(segment.path)); continue; } $appendTextWithLineBreaks(paragraph, segment.text); }} export interface ComposerPromptEditorHandle { focus: () => void; focusAt: (cursor: number) => void; focusAtEnd: () => void; readSnapshot: () => { value: string; cursor: number };} interface ComposerPromptEditorProps { value: string; cursor: number; disabled: boolean; placeholder: string; className?: string; onChange: (nextValue: string, nextCursor: number, cursorAdjacentToMention: boolean) => void; onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent, ) => boolean; onPaste: ClipboardEventHandler<HTMLElement>;} interface ComposerPromptEditorInnerProps extends ComposerPromptEditorProps { editorRef: Ref<ComposerPromptEditorHandle>;} function ComposerCommandKeyPlugin(props: { onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent, ) => boolean;}) { const [editor] = useLexicalComposerContext(); useEffect(() => { const handleCommand = ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent | null, ): boolean => { if (!props.onCommandKeyDown || !event) { return false; } const handled = props.onCommandKeyDown(key, event); if (handled) { event.preventDefault(); event.stopPropagation(); } return handled; }; const unregisterArrowDown = editor.registerCommand( KEY_ARROW_DOWN_COMMAND, (event) => handleCommand("ArrowDown", event), COMMAND_PRIORITY_HIGH, ); const unregisterArrowUp = editor.registerCommand( KEY_ARROW_UP_COMMAND, (event) => handleCommand("ArrowUp", event), COMMAND_PRIORITY_HIGH, ); const unregisterEnter = editor.registerCommand( KEY_ENTER_COMMAND, (event) => handleCommand("Enter", event), COMMAND_PRIORITY_HIGH, ); const unregisterTab = editor.registerCommand( KEY_TAB_COMMAND, (event) => handleCommand("Tab", event), COMMAND_PRIORITY_HIGH, ); return () => { unregisterArrowDown(); unregisterArrowUp(); unregisterEnter(); unregisterTab(); }; }, [editor, props]); return null;} function ComposerMentionArrowPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { const unregisterLeft = editor.registerCommand( KEY_ARROW_LEFT_COMMAND, (event) => { let nextOffset: number | null = null; editor.getEditorState().read(() => { const selection = $getSelection(); if (!$isRangeSelection(selection) || !selection.isCollapsed()) return; const currentOffset = $readSelectionOffsetFromEditorState(0); if (currentOffset <= 0) return; const promptValue = $getRoot().getTextContent(); if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "left")) { return; } nextOffset = currentOffset - 1; }); if (nextOffset === null) return false; const selectionOffset = nextOffset; event?.preventDefault(); event?.stopPropagation(); editor.update(() => { $setSelectionAtComposerOffset(selectionOffset); }); return true; }, COMMAND_PRIORITY_HIGH, ); const unregisterRight = editor.registerCommand( KEY_ARROW_RIGHT_COMMAND, (event) => { let nextOffset: number | null = null;Lines 1–500 of 811. Load more
import { LexicalComposer, type InitialConfigType, } from "@lexical/react/LexicalComposer"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; import { $applyNodeReplacement, $createRangeSelection, $getSelection, $setSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, $createLineBreakNode, $createParagraphNode, $createTextNode, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, COMMAND_PRIORITY_HIGH, KEY_BACKSPACE_COMMAND, $getRoot, type ElementNode, type LexicalNode, TextNode, type EditorConfig, type EditorState, type NodeKey, type SerializedTextNode, type Spread, } from "lexical"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, type ClipboardEventHandler, 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"; const COMPOSER_EDITOR_HMR_KEY = `composer-editor-${Math.random().toString(36).slice(2)}`; type SerializedComposerMentionNode = Spread< { path: string; type: "composer-mention"; version: 1; }, SerializedTextNode >; class ComposerMentionNode extends TextNode { __path: string; static override getType(): string { return "composer-mention"; } static override clone(node: ComposerMentionNode): ComposerMentionNode { return new ComposerMentionNode(node.__path, node.__key); } static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode { return $createComposerMentionNode(serializedNode.path); } constructor(path: string, key?: NodeKey) { const normalizedPath = path.startsWith("@") ? path.slice(1) : path; super(`@${normalizedPath}`, key); this.__path = normalizedPath; } override exportJSON(): SerializedComposerMentionNode { return { ...super.exportJSON(), path: this.__path, type: "composer-mention", version: 1, }; } override createDOM(_config: EditorConfig): HTMLElement { const dom = document.createElement("span"); dom.className = "inline-flex select-none items-center gap-1 rounded-md border border-border/70 bg-accent/40 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-foreground align-middle"; dom.contentEditable = "false"; dom.setAttribute("spellcheck", "false"); renderMentionChipDom(dom, this.__path); return dom; } override updateDOM( prevNode: ComposerMentionNode, dom: HTMLElement, _config: EditorConfig, ): boolean { dom.contentEditable = "false"; if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) { renderMentionChipDom(dom, this.__path); } return false; } override canInsertTextBefore(): false { return false; } override canInsertTextAfter(): false { return false; } override isTextEntity(): true { return true; } override isToken(): true { return true; } } function $createComposerMentionNode(path: string): ComposerMentionNode { return $applyNodeReplacement(new ComposerMentionNode(path)); } function inferMentionPathKind(pathValue: string): "file" | "directory" { const base = basenameOfPath(pathValue); if (base.startsWith(".") && !base.slice(1).includes(".")) { return "directory"; } if (base.includes(".")) { return "file"; } return "directory"; } function resolvedThemeFromDocument(): "light" | "dark" { return document.documentElement.classList.contains("dark") ? "dark" : "light"; } function renderMentionChipDom(container: HTMLElement, pathValue: string): void { container.textContent = ""; container.style.setProperty("user-select", "none"); container.style.setProperty("-webkit-user-select", "none"); const theme = resolvedThemeFromDocument(); const icon = document.createElement("img"); icon.alt = ""; icon.ariaHidden = "true"; icon.className = "size-3.5 shrink-0 opacity-85"; icon.loading = "lazy"; icon.src = getVscodeIconUrlForEntry(pathValue, inferMentionPathKind(pathValue), theme); const label = document.createElement("span"); label.className = "truncate select-none leading-tight"; label.textContent = basenameOfPath(pathValue); container.append(icon, label); } function clampCursor(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 { if (node instanceof ComposerMentionNode) { return 1; } if ($isTextNode(node)) { return node.getTextContentSize(); } if ($isLineBreakNode(node)) { return 1; } if ($isElementNode(node)) { return node.getChildren().reduce((total, child) => total + getComposerNodeTextLength(child), 0); } return 0; } function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number { let offset = 0; let current: LexicalNode | null = node; while (current) { const nextParent = current.getParent() as LexicalNode | null; if (!nextParent || !$isElementNode(nextParent)) { break; } const siblings = nextParent.getChildren(); const index = current.getIndexWithinParent(); for (let i = 0; i < index; i += 1) { const sibling = siblings[i]; if (!sibling) continue; offset += getComposerNodeTextLength(sibling); } current = nextParent; } if ($isTextNode(node)) { if (node instanceof ComposerMentionNode) { return offset + (pointOffset > 0 ? 1 : 0); } return offset + Math.min(pointOffset, node.getTextContentSize()); } if ($isLineBreakNode(node)) { return offset + Math.min(pointOffset, 1); } if ($isElementNode(node)) { const children = node.getChildren(); const clampedOffset = Math.max(0, Math.min(pointOffset, children.length)); for (let i = 0; i < clampedOffset; i += 1) { const child = children[i]; if (!child) continue; offset += getComposerNodeTextLength(child); } return offset; } return offset; } function findSelectionPointAtOffset( node: LexicalNode, remainingRef: { value: number }, ): { key: string; offset: number; type: "text" | "element" } | null { if (node instanceof ComposerMentionNode) { const parent = node.getParent(); if (!parent || !$isElementNode(parent)) return null; const index = node.getIndexWithinParent(); if (remainingRef.value === 0) { return { key: parent.getKey(), offset: index, type: "element", }; } if (remainingRef.value === 1) { return { key: parent.getKey(), offset: index + 1, type: "element", }; } remainingRef.value -= 1; return null; } if ($isTextNode(node)) { const size = node.getTextContentSize(); if (remainingRef.value <= size) { return { key: node.getKey(), offset: remainingRef.value, type: "text", }; } remainingRef.value -= size; return null; } if ($isLineBreakNode(node)) { const parent = node.getParent(); if (!parent) return null; const index = node.getIndexWithinParent(); if (remainingRef.value === 0) { return { key: parent.getKey(), offset: index, type: "element", }; } if (remainingRef.value === 1) { return { key: parent.getKey(), offset: index + 1, type: "element", }; } remainingRef.value -= 1; return null; } if ($isElementNode(node)) { const children = node.getChildren(); for (const child of children) { const point = findSelectionPointAtOffset(child, remainingRef); if (point) { return point; } } if (remainingRef.value === 0) { return { key: node.getKey(), offset: children.length, type: "element", }; } } return null; } function $getComposerRootLength(): number { const root = $getRoot(); const children = root.getChildren(); return children.reduce((sum, child) => sum + getComposerNodeTextLength(child), 0); } function $setSelectionAtComposerOffset(nextOffset: number): void { const root = $getRoot(); const composerLength = $getComposerRootLength(); const boundedOffset = Math.max(0, Math.min(nextOffset, composerLength)); const remainingRef = { value: boundedOffset }; const point = findSelectionPointAtOffset(root, remainingRef) ?? { key: root.getKey(), offset: root.getChildren().length, type: "element" as const, }; const selection = $createRangeSelection(); selection.anchor.set(point.key, point.offset, point.type); selection.focus.set(point.key, point.offset, point.type); $setSelection(selection); } function $readSelectionOffsetFromEditorState(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); const composerLength = $getComposerRootLength(); return Math.max(0, Math.min(offset, composerLength)); } function $appendTextWithLineBreaks(parent: ElementNode, text: string): void { const lines = text.split("\n"); for (let index = 0; index < lines.length; index += 1) { const line = lines[index] ?? ""; if (line.length > 0) { parent.append($createTextNode(line)); } if (index < lines.length - 1) { parent.append($createLineBreakNode()); } } } function $setComposerEditorPrompt(prompt: string): void { const root = $getRoot(); root.clear(); const paragraph = $createParagraphNode(); root.append(paragraph); const segments = splitPromptIntoComposerSegments(prompt); for (const segment of segments) { if (segment.type === "mention") { paragraph.append($createComposerMentionNode(segment.path)); continue; } $appendTextWithLineBreaks(paragraph, segment.text); } } export interface ComposerPromptEditorHandle { focus: () => void; focusAt: (cursor: number) => void; focusAtEnd: () => void; readSnapshot: () => { value: string; cursor: number }; } interface ComposerPromptEditorProps { value: string; cursor: number; disabled: boolean; placeholder: string; className?: string; onChange: (nextValue: string, nextCursor: number, cursorAdjacentToMention: boolean) => void; onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent, ) => boolean; onPaste: ClipboardEventHandler<HTMLElement>; } interface ComposerPromptEditorInnerProps extends ComposerPromptEditorProps { editorRef: Ref<ComposerPromptEditorHandle>; } function ComposerCommandKeyPlugin(props: { onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent, ) => boolean; }) { const [editor] = useLexicalComposerContext(); useEffect(() => { const handleCommand = ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent | null, ): boolean => { if (!props.onCommandKeyDown || !event) { return false; } const handled = props.onCommandKeyDown(key, event); if (handled) { event.preventDefault(); event.stopPropagation(); } return handled; }; const unregisterArrowDown = editor.registerCommand( KEY_ARROW_DOWN_COMMAND, (event) => handleCommand("ArrowDown", event), COMMAND_PRIORITY_HIGH, ); const unregisterArrowUp = editor.registerCommand( KEY_ARROW_UP_COMMAND, (event) => handleCommand("ArrowUp", event), COMMAND_PRIORITY_HIGH, ); const unregisterEnter = editor.registerCommand( KEY_ENTER_COMMAND, (event) => handleCommand("Enter", event), COMMAND_PRIORITY_HIGH, ); const unregisterTab = editor.registerCommand( KEY_TAB_COMMAND, (event) => handleCommand("Tab", event), COMMAND_PRIORITY_HIGH, ); return () => { unregisterArrowDown(); unregisterArrowUp(); unregisterEnter(); unregisterTab(); }; }, [editor, props]); return null; } function ComposerMentionArrowPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { const unregisterLeft = editor.registerCommand( KEY_ARROW_LEFT_COMMAND, (event) => { let nextOffset: number | null = null; editor.getEditorState().read(() => { const selection = $getSelection(); if (!$isRangeSelection(selection) || !selection.isCollapsed()) return; const currentOffset = $readSelectionOffsetFromEditorState(0); if (currentOffset <= 0) return; const promptValue = $getRoot().getTextContent(); if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "left")) { return; } nextOffset = currentOffset - 1; }); if (nextOffset === null) return false; const selectionOffset = nextOffset; event?.preventDefault(); event?.stopPropagation(); editor.update(() => { $setSelectionAtComposerOffset(selectionOffset); }); return true; }, COMMAND_PRIORITY_HIGH, ); const unregisterRight = editor.registerCommand( KEY_ARROW_RIGHT_COMMAND, (event) => { let nextOffset: number | null = null;