diff --git a/ui/src/components/DocumentAnnotationLayer.tsx b/ui/src/components/DocumentAnnotationLayer.tsx index 6859922ab60..a31bcdc777f 100644 --- a/ui/src/components/DocumentAnnotationLayer.tsx +++ b/ui/src/components/DocumentAnnotationLayer.tsx @@ -48,8 +48,17 @@ export interface AnnotationLayerProps { * pending anchor for the keyboard shortcut path. */ captureSelectionRequestId?: number; + /** + * Text of a comment currently being composed. We keep this segment brightly + * highlighted in the document even after the native browser selection is lost + * (e.g. once focus moves into the composer textarea). + */ + pendingHighlightText?: string | null; } +/** Synthetic thread id used to render the in-progress (pending) comment highlight. */ +const PENDING_HIGHLIGHT_THREAD_ID = "__paperclip-pending-annotation__"; + interface HighlightRect { threadId: string; status: DocumentAnnotationThreadStatus; @@ -60,6 +69,8 @@ interface HighlightRect { height: number; /** True for the last rect of this thread (used to anchor a glyph at the run end). */ isTail: boolean; + /** True when this run should render with the brighter focused/pending treatment. */ + focused: boolean; } interface ToolbarPosition { @@ -203,6 +214,7 @@ export function DocumentAnnotationLayer({ newCommentDisabledReason = null, hideResolved = true, captureSelectionRequestId, + pendingHighlightText = null, }: AnnotationLayerProps) { const [highlightRects, setHighlightRects] = useState([]); const [toolbarPosition, setToolbarPosition] = useState(null); @@ -231,19 +243,17 @@ export function DocumentAnnotationLayer({ const overlayRect = overlay.getBoundingClientRect(); const next: HighlightRect[] = []; const nativeRanges = emptyNativeHighlightRanges(); - for (const thread of visibleThreads) { - if (thread.anchorState === "orphaned") continue; - const isFocused = thread.id === focusedThreadId; - const isStale = thread.anchorState === "stale"; - const isResolved = thread.status === "resolved"; - const nativeKind = nativeHighlightKind({ - focused: isFocused, - stale: isStale, - resolved: isResolved, - }); + const pushRunRects = (run: { + threadId: string; + status: DocumentAnnotationThreadStatus; + anchorState: DocumentAnnotationAnchorState; + focused: boolean; + selectedText: string; + nativeKind: NativeHighlightKind; + }) => { const ranges = rangesForNormalizedSpan({ container, - selectedText: thread.selectedText, + selectedText: run.selectedText, }); const startIndex = next.length; for (const range of ranges) { @@ -256,9 +266,10 @@ export function DocumentAnnotationLayer({ if (!visibleRect) continue; rangeIsVisible = true; next.push({ - threadId: thread.id, - status: thread.status, - anchorState: thread.anchorState, + threadId: run.threadId, + status: run.status, + anchorState: run.anchorState, + focused: run.focused, top: visibleRect.top - overlayRect.top, left: visibleRect.left - overlayRect.left, width: visibleRect.width, @@ -266,15 +277,41 @@ export function DocumentAnnotationLayer({ isTail: false, }); } - if (rangeIsVisible) nativeRanges[nativeKind].push(range); + if (rangeIsVisible) nativeRanges[run.nativeKind].push(range); } if (next.length > startIndex) { next[next.length - 1].isTail = true; } + }; + for (const thread of visibleThreads) { + if (thread.anchorState === "orphaned") continue; + const isFocused = thread.id === focusedThreadId; + const isStale = thread.anchorState === "stale"; + const isResolved = thread.status === "resolved"; + pushRunRects({ + threadId: thread.id, + status: thread.status, + anchorState: thread.anchorState, + focused: isFocused, + selectedText: thread.selectedText, + nativeKind: nativeHighlightKind({ focused: isFocused, stale: isStale, resolved: isResolved }), + }); + } + // Keep the in-progress (pending) comment selection brightly highlighted so the + // segment stays anchored in the document while the composer is open. + if (pendingHighlightText && pendingHighlightText.trim().length > 0) { + pushRunRects({ + threadId: PENDING_HIGHLIGHT_THREAD_ID, + status: "open", + anchorState: "active", + focused: true, + selectedText: pendingHighlightText, + nativeKind: "focused", + }); } setNativeHighlightRanges(nativeHighlightInstanceId, nativeRanges); setHighlightRects(next); - }, [containerRef, focusedThreadId, nativeHighlightInstanceId, visibleThreads]); + }, [containerRef, focusedThreadId, nativeHighlightInstanceId, pendingHighlightText, visibleThreads]); useLayoutEffect(() => { computeHighlightRects(); @@ -398,7 +435,7 @@ export function DocumentAnnotationLayer({