diff --git a/packages/ui/src/components/ui/CodeMirrorEditor.tsx b/packages/ui/src/components/ui/CodeMirrorEditor.tsx index bc4dcc30..09972fc3 100644 --- a/packages/ui/src/components/ui/CodeMirrorEditor.tsx +++ b/packages/ui/src/components/ui/CodeMirrorEditor.tsx @@ -16,6 +16,8 @@ type CodeMirrorEditorProps = { readOnly?: boolean; lineNumbersConfig?: Parameters[0]; highlightLines?: { start: number; end: number }; + onViewReady?: (view: EditorView) => void; + onViewDestroy?: () => void; }; const lineNumbersCompartment = new Compartment(); @@ -55,11 +57,23 @@ const createHighlightLinesExtension = (range?: { start: number; end: number }): }, { decorations: (v) => v.decorations }); }; -export function CodeMirrorEditor({ value, onChange, extensions, className, readOnly, lineNumbersConfig, highlightLines }: CodeMirrorEditorProps) { +export function CodeMirrorEditor({ + value, + onChange, + extensions, + className, + readOnly, + lineNumbersConfig, + highlightLines, + onViewReady, + onViewDestroy, +}: CodeMirrorEditorProps) { const hostRef = React.useRef(null); const viewRef = React.useRef(null); const valueRef = React.useRef(value); const onChangeRef = React.useRef(onChange); + const onViewReadyRef = React.useRef(onViewReady); + const onViewDestroyRef = React.useRef(onViewDestroy); React.useEffect(() => { valueRef.current = value; @@ -69,6 +83,11 @@ export function CodeMirrorEditor({ value, onChange, extensions, className, readO onChangeRef.current = onChange; }, [onChange]); + React.useEffect(() => { + onViewReadyRef.current = onViewReady; + onViewDestroyRef.current = onViewDestroy; + }, [onViewReady, onViewDestroy]); + React.useEffect(() => { if (!hostRef.current) { return; @@ -101,7 +120,12 @@ export function CodeMirrorEditor({ value, onChange, extensions, className, readO parent: hostRef.current, }); + if (viewRef.current) { + onViewReadyRef.current?.(viewRef.current); + } + return () => { + onViewDestroyRef.current?.(); viewRef.current?.destroy(); viewRef.current = null; }; diff --git a/packages/ui/src/components/views/PierreDiffViewer.tsx b/packages/ui/src/components/views/PierreDiffViewer.tsx index f937ea2a..6a1dac68 100644 --- a/packages/ui/src/components/views/PierreDiffViewer.tsx +++ b/packages/ui/src/components/views/PierreDiffViewer.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { FileDiff as PierreFileDiff, type FileContents, type FileDiffOptions, type SelectedLineRange } from '@pierre/diffs'; -import { RiMoreLine, RiDeleteBinLine } from '@remixicon/react'; +import { RiMoreLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; @@ -173,7 +173,10 @@ export const PierreDiffViewer: React.FC = ({ const [selection, setSelection] = useState(null); const [commentText, setCommentText] = useState(''); + const [editingDraftId, setEditingDraftId] = useState(null); + const [pendingFocus, setPendingFocus] = useState(false); const commentContainerRef = useRef(null); + const commentInputRef = useRef(null); // Refs to prevent infinite loops when syncing selection with diff instance const selectionRef = useRef(null); @@ -208,6 +211,7 @@ export const PierreDiffViewer: React.FC = ({ // Inline comment drafts const addDraft = useInlineCommentDraftStore(state => state.addDraft); + const updateDraft = useInlineCommentDraftStore(state => state.updateDraft); const removeDraft = useInlineCommentDraftStore(state => state.removeDraft); const allDrafts = useInlineCommentDraftStore(state => state.drafts); @@ -281,6 +285,8 @@ export const PierreDiffViewer: React.FC = ({ setSelection(range); if (!range) { setCommentText(''); + setEditingDraftId(null); + setPendingFocus(false); } }, [isMobile]); @@ -309,6 +315,8 @@ export const PierreDiffViewer: React.FC = ({ if (!isLineNumber) { setSelection(null); setCommentText(''); + setEditingDraftId(null); + setPendingFocus(false); } }; @@ -326,8 +334,25 @@ export const PierreDiffViewer: React.FC = ({ const handleCancelComment = useCallback(() => { setCommentText(''); setSelection(null); + setEditingDraftId(null); + setPendingFocus(false); }, []); + useEffect(() => { + if (!pendingFocus || !selection || isMobile) return; + if (typeof window === 'undefined') return; + const frame = window.requestAnimationFrame(() => { + const input = commentInputRef.current; + input?.focus(); + if (input) { + const length = input.value.length; + input.setSelectionRange(length, length); + } + setPendingFocus(false); + }); + return () => window.cancelAnimationFrame(frame); + }, [pendingFocus, selection, isMobile]); + const handleSaveComment = useCallback(() => { if (!selection || !commentText.trim()) return; @@ -340,24 +365,52 @@ export const PierreDiffViewer: React.FC = ({ const code = extractSelectedCode(original, modified, selection); const side = selection.side === 'deletions' ? 'original' : 'modified'; - addDraft({ - sessionKey, - source: 'diff', - fileLabel: fileName, - startLine: selection.start, - endLine: selection.end, - side, - code, - language, - text: commentText.trim(), - }); + if (editingDraftId) { + updateDraft(sessionKey, editingDraftId, { + fileLabel: fileName, + startLine: selection.start, + endLine: selection.end, + side, + code, + language, + text: commentText.trim(), + }); + } else { + addDraft({ + sessionKey, + source: 'diff', + fileLabel: fileName, + startLine: selection.start, + endLine: selection.end, + side, + code, + language, + text: commentText.trim(), + }); + } // Clear selection and comment text setCommentText(''); setSelection(null); + setEditingDraftId(null); - toast.success('Comment saved'); - }, [selection, commentText, original, modified, fileName, language, addDraft, getSessionKey]); + toast.success(editingDraftId ? 'Comment updated' : 'Comment saved'); + }, [selection, commentText, original, modified, fileName, language, addDraft, updateDraft, getSessionKey, editingDraftId]); + + const applySelection = useCallback((range: SelectedLineRange) => { + setSelection(range); + const instance = diffInstanceRef.current; + if (!instance) return; + try { + isApplyingSelectionRef.current = true; + instance.setSelectedLines(range); + lastAppliedSelectionRef.current = range; + } catch { + // ignore + } finally { + isApplyingSelectionRef.current = false; + } + }, []); ensurePierreThemeRegistered(lightTheme); ensurePierreThemeRegistered(darkTheme); @@ -558,6 +611,7 @@ export const PierreDiffViewer: React.FC = ({ > {/* Textarea - auto-grows from 1 line to max 5 lines */}