fix: cursor position after slash commands

This commit is contained in:
Johnny 2026-01-05 22:24:13 +08:00
parent 15646a8989
commit 4b110d0d38
4 changed files with 27 additions and 11 deletions

View File

@ -1,21 +1,26 @@
import type { SlashCommandsProps } from "../types";
import type { EditorRefActions } from ".";
import { SuggestionsPopup } from "./SuggestionsPopup";
import { useSuggestions } from "./useSuggestions";
const SlashCommands = ({ editorRef, editorActions, commands }: SlashCommandsProps) => {
const handleCommandAutocomplete = (cmd: (typeof commands)[0], word: string, index: number, actions: EditorRefActions) => {
// Remove trigger char + word, then insert command output
actions.removeText(index, word.length);
actions.insertText(cmd.run());
// Position cursor relative to insertion point, if specified
if (cmd.cursorOffset) {
actions.setCursorPosition(index + cmd.cursorOffset);
}
};
const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({
editorRef,
editorActions,
triggerChar: "/",
items: commands,
filterItems: (items, query) => (!query ? items : items.filter((cmd) => cmd.name.toLowerCase().startsWith(query))),
onAutocomplete: (cmd, word, index, actions) => {
actions.removeText(index, word.length);
actions.insertText(cmd.run());
if (cmd.cursorOffset) {
actions.setCursorPosition(actions.getCursorPosition() + cmd.cursorOffset);
}
},
onAutocomplete: handleCommandAutocomplete,
});
if (!isVisible || !position) return null;

View File

@ -98,7 +98,7 @@ const Editor = forwardRef(function Editor(props: EditorProps, ref: React.Forward
editor.value = editor.value.slice(0, start) + editor.value.slice(start + length);
editor.focus();
editor.selectionEnd = start;
editor.setSelectionRange(start, start);
updateContent();
},
setContent: (text: string) => {
@ -116,8 +116,11 @@ const Editor = forwardRef(function Editor(props: EditorProps, ref: React.Forward
return editor.value.slice(editor.selectionStart, editor.selectionEnd);
},
setCursorPosition: (startPos: number, endPos?: number) => {
const endPosition = Number.isNaN(endPos) ? startPos : (endPos as number);
editorRef.current?.setSelectionRange(startPos, endPosition);
const editor = editorRef.current;
if (!editor) return;
// setSelectionRange requires valid arguments; default to startPos if endPos is undefined
const endPosition = endPos !== undefined && !Number.isNaN(endPos) ? endPos : startPos;
editor.setSelectionRange(startPos, endPosition);
},
getCursorLineNumber: () => {
const editor = editorRef.current;

View File

@ -35,6 +35,7 @@ export function useSuggestions<T>({
}: UseSuggestionsOptions<T>): UseSuggestionsReturn<T> {
const [position, setPosition] = useState<Position | null>(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const isProcessingRef = useRef(false);
const selectedRef = useRef(selectedIndex);
selectedRef.current = selectedIndex;
@ -66,9 +67,14 @@ export function useSuggestions<T>({
console.warn("useSuggestions: editorActions not available");
return;
}
isProcessingRef.current = true;
const [word, index] = getCurrentWord();
onAutocomplete(item, word, index, editorActions.current);
hide();
// Re-enable input handling after all DOM operations complete
queueMicrotask(() => {
isProcessingRef.current = false;
});
};
const handleNavigation = (e: KeyboardEvent, selected: number, suggestionsCount: number) => {
@ -107,6 +113,8 @@ export function useSuggestions<T>({
};
const handleInput = () => {
if (isProcessingRef.current) return;
const editor = editorRef.current;
if (!editor) return;

View File

@ -120,7 +120,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
<div className="p-2 flex flex-col gap-1">
{mediaItems.length > 0 && <MediaGrid attachments={mediaItems} onImageClick={handleImageClick} />}
{mediaItems.length > 0 && docItems.length > 0 && <div className="border-t border-border opacity-60" />}
{mediaItems.length > 0 && docItems.length > 0 && <div className="border-t mt-1 border-border opacity-60" />}
{docItems.length > 0 && <DocsList attachments={docItems} />}
</div>