diff options
Diffstat (limited to 'packages/ui/src/components/session-review.tsx')
| -rw-r--r-- | packages/ui/src/components/session-review.tsx | 376 |
1 files changed, 6 insertions, 370 deletions
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 62c70e864..49c561e0b 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -9,9 +9,6 @@ import { IconButton } from "./icon-button" import { StickyAccordionHeader } from "./sticky-accordion-header" import { Tooltip } from "./tooltip" import { ScrollView } from "./scroll-view" -import { FileSearchBar } from "./file-search" -import type { FileSearchHandle } from "./file" -import { buildSessionSearchHits, stepSessionSearchIndex, type SessionSearchHit } from "./session-review-search" import { useFileComponent } from "../context/file" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -63,6 +60,8 @@ export type SessionReviewCommentActions = { export type SessionReviewFocus = { file: string; id: string } +type ReviewDiff = FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> } + export interface SessionReviewProps { title?: JSX.Element empty?: JSX.Element @@ -86,7 +85,7 @@ export interface SessionReviewProps { classList?: Record<string, boolean | undefined> classes?: { root?: string; header?: string; container?: string } actions?: JSX.Element - diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[] + diffs: ReviewDiff[] onViewFile?: (file: string) => void readFile?: (path: string) => Promise<FileContent | undefined> } @@ -135,15 +134,10 @@ type SessionReviewSelection = { export const SessionReview = (props: SessionReviewProps) => { let scroll: HTMLDivElement | undefined - let searchInput: HTMLInputElement | undefined let focusToken = 0 - let revealToken = 0 - let highlightedFile: string | undefined const i18n = useI18n() const fileComponent = useFileComponent() const anchors = new Map<string, HTMLElement>() - const searchHandles = new Map<string, FileSearchHandle>() - const readyFiles = new Set<string>() const [store, setStore] = createStore<{ open: string[]; force: Record<string, boolean> }>({ open: [], force: {}, @@ -152,18 +146,12 @@ export const SessionReview = (props: SessionReviewProps) => { const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null) const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null) const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null) - const [searchOpen, setSearchOpen] = createSignal(false) - const [searchQuery, setSearchQuery] = createSignal("") - const [searchActive, setSearchActive] = createSignal(0) - const [searchPos, setSearchPos] = createSignal({ top: 8, right: 8 }) const open = () => props.open ?? store.open - const files = createMemo(() => props.diffs.map((d) => d.file)) - const diffs = createMemo(() => new Map(props.diffs.map((d) => [d.file, d] as const))) + const files = createMemo(() => props.diffs.map((diff) => diff.file)) + const diffs = createMemo(() => new Map(props.diffs.map((diff) => [diff.file, diff] as const))) const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified") const hasDiffs = () => files().length > 0 - const searchValue = createMemo(() => searchQuery().trim()) - const searchExpanded = createMemo(() => searchValue().length > 0) const handleChange = (open: string[]) => { props.onOpenChange?.(open) @@ -176,266 +164,8 @@ export const SessionReview = (props: SessionReviewProps) => { handleChange(next) } - const clearViewerSearch = () => { - for (const handle of searchHandles.values()) handle.clear() - highlightedFile = undefined - } - const openFileLabel = () => i18n.t("ui.sessionReview.openFile") - const selectionLabel = (range: SelectedLineRange) => { - const start = Math.min(range.start, range.end) - const end = Math.max(range.start, range.end) - if (start === end) return i18n.t("ui.sessionReview.selection.line", { line: start }) - return i18n.t("ui.sessionReview.selection.lines", { start, end }) - } - - const focusSearch = () => { - if (!hasDiffs()) return - setSearchOpen(true) - requestAnimationFrame(() => { - searchInput?.focus() - searchInput?.select() - }) - } - - const closeSearch = () => { - revealToken++ - setSearchOpen(false) - setSearchQuery("") - setSearchActive(0) - clearViewerSearch() - } - - const positionSearchBar = () => { - if (typeof window === "undefined") return - if (!scroll) return - - const rect = scroll.getBoundingClientRect() - const title = parseFloat(getComputedStyle(scroll).getPropertyValue("--session-title-height")) - const header = Number.isNaN(title) ? 0 : title - setSearchPos({ - top: Math.round(rect.top) + header - 4, - right: Math.round(window.innerWidth - rect.right) + 8, - }) - } - - const searchHits = createMemo(() => - buildSessionSearchHits({ - query: searchQuery(), - files: props.diffs.flatMap((diff) => { - if (mediaKindFromPath(diff.file)) return [] - - return [ - { - file: diff.file, - before: typeof diff.before === "string" ? diff.before : undefined, - after: typeof diff.after === "string" ? diff.after : undefined, - }, - ] - }), - }), - ) - - const waitForViewer = (file: string, token: number) => - new Promise<FileSearchHandle | undefined>((resolve) => { - let attempt = 0 - - const tick = () => { - if (token !== revealToken) { - resolve(undefined) - return - } - - const handle = searchHandles.get(file) - if (handle && readyFiles.has(file)) { - resolve(handle) - return - } - - if (attempt >= 180) { - resolve(undefined) - return - } - - attempt++ - requestAnimationFrame(tick) - } - - tick() - }) - - const waitForFrames = (count: number, token: number) => - new Promise<boolean>((resolve) => { - const tick = (left: number) => { - if (token !== revealToken) { - resolve(false) - return - } - - if (left <= 0) { - resolve(true) - return - } - - requestAnimationFrame(() => tick(left - 1)) - } - - tick(count) - }) - - const revealSearchHit = async (token: number, hit: SessionSearchHit, query: string) => { - const diff = diffs().get(hit.file) - if (!diff) return - - if (!open().includes(hit.file)) { - handleChange([...open(), hit.file]) - } - - if (!mediaKindFromPath(hit.file) && diff.additions + diff.deletions > MAX_DIFF_CHANGED_LINES) { - setStore("force", hit.file, true) - } - - const handle = await waitForViewer(hit.file, token) - if (!handle || token !== revealToken) return - if (searchValue() !== query) return - if (!(await waitForFrames(2, token))) return - - if (highlightedFile && highlightedFile !== hit.file) { - searchHandles.get(highlightedFile)?.clear() - highlightedFile = undefined - } - - anchors.get(hit.file)?.scrollIntoView({ block: "nearest" }) - - let done = false - for (let i = 0; i < 4; i++) { - if (token !== revealToken) return - if (searchValue() !== query) return - - handle.setQuery(query) - if (handle.reveal(hit)) { - done = true - break - } - - const expanded = handle.expand(hit) - handle.refresh() - if (!(await waitForFrames(expanded ? 2 : 1, token))) return - } - - if (!done) return - - if (!(await waitForFrames(1, token))) return - handle.reveal(hit) - - highlightedFile = hit.file - } - - const navigateSearch = (dir: 1 | -1) => { - const total = searchHits().length - if (total <= 0) return - setSearchActive((value) => stepSessionSearchIndex(total, value, dir)) - } - - const inReview = (node: unknown, path?: unknown[]) => { - if (node === searchInput) return true - if (path?.some((item) => item === scroll || item === searchInput)) return true - if (path?.some((item) => item instanceof HTMLElement && item.dataset.component === "session-review")) { - return true - } - if (!(node instanceof Node)) return false - if (searchInput?.contains(node)) return true - if (node instanceof HTMLElement && node.closest("[data-component='session-review']")) return true - if (!scroll) return false - return scroll.contains(node) - } - - createEffect(() => { - if (typeof window === "undefined") return - - const onKeyDown = (event: KeyboardEvent) => { - const mod = event.metaKey || event.ctrlKey - if (!mod) return - - const key = event.key.toLowerCase() - if (key !== "f" && key !== "g") return - - if (key === "f") { - if (!hasDiffs()) return - event.preventDefault() - event.stopPropagation() - focusSearch() - return - } - - const path = typeof event.composedPath === "function" ? event.composedPath() : undefined - if (!inReview(event.target, path) && !inReview(document.activeElement, path)) return - if (!searchOpen()) return - event.preventDefault() - event.stopPropagation() - navigateSearch(event.shiftKey ? -1 : 1) - } - - window.addEventListener("keydown", onKeyDown, { capture: true }) - onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true })) - }) - - createEffect(() => { - diffStyle() - searchExpanded() - readyFiles.clear() - }) - - createEffect(() => { - if (!searchOpen()) return - if (!scroll) return - - const root = scroll - - requestAnimationFrame(positionSearchBar) - window.addEventListener("resize", positionSearchBar, { passive: true }) - const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(positionSearchBar) - observer?.observe(root) - - onCleanup(() => { - window.removeEventListener("resize", positionSearchBar) - observer?.disconnect() - }) - }) - - createEffect(() => { - const total = searchHits().length - if (total === 0) { - if (searchActive() !== 0) setSearchActive(0) - return - } - - if (searchActive() >= total) setSearchActive(total - 1) - }) - - createEffect(() => { - diffStyle() - const query = searchValue() - const hits = searchHits() - const token = ++revealToken - if (!query || hits.length === 0) { - clearViewerSearch() - return - } - - const hit = hits[Math.min(searchActive(), hits.length - 1)] - if (!hit) return - void revealSearchHit(token, hit, query) - }) - - onCleanup(() => { - revealToken++ - clearViewerSearch() - readyFiles.clear() - searchHandles.clear() - }) - const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions" const selectionPreview = (diff: FileDiff, range: SelectedLineRange) => { @@ -499,58 +229,6 @@ export const SessionReview = (props: SessionReviewProps) => { }) }) - const handleReviewKeyDown = (event: KeyboardEvent) => { - if (event.defaultPrevented) return - - const mod = event.metaKey || event.ctrlKey - const key = event.key.toLowerCase() - const target = event.target - if (mod && key === "f") { - event.preventDefault() - event.stopPropagation() - focusSearch() - return - } - - if (mod && key === "g") { - if (!searchOpen()) return - event.preventDefault() - event.stopPropagation() - navigateSearch(event.shiftKey ? -1 : 1) - } - } - - const handleSearchInputKeyDown = (event: KeyboardEvent) => { - const mod = event.metaKey || event.ctrlKey - const key = event.key.toLowerCase() - - if (mod && key === "g") { - event.preventDefault() - event.stopPropagation() - navigateSearch(event.shiftKey ? -1 : 1) - return - } - - if (mod && key === "f") { - event.preventDefault() - event.stopPropagation() - focusSearch() - return - } - - if (event.key === "Escape") { - event.preventDefault() - event.stopPropagation() - closeSearch() - return - } - - if (event.key !== "Enter") return - event.preventDefault() - event.stopPropagation() - navigateSearch(event.shiftKey ? -1 : 1) - } - return ( <div data-component="session-review" class={props.class} classList={props.classList}> <div data-slot="session-review-header" class={props.classes?.header}> @@ -594,31 +272,10 @@ export const SessionReview = (props: SessionReviewProps) => { props.scrollRef?.(el) }} onScroll={props.onScroll as any} - onKeyDown={handleReviewKeyDown} classList={{ [props.classes?.root ?? ""]: !!props.classes?.root, }} > - <Show when={searchOpen()}> - <FileSearchBar - pos={searchPos} - query={searchQuery} - index={() => (searchHits().length ? Math.min(searchActive(), searchHits().length - 1) : 0)} - count={() => searchHits().length} - setInput={(el) => { - searchInput = el - }} - onInput={(value) => { - setSearchQuery(value) - setSearchActive(0) - }} - onKeyDown={(event) => handleSearchInputKeyDown(event)} - onClose={closeSearch} - onPrev={() => navigateSearch(-1)} - onNext={() => navigateSearch(1)} - /> - </Show> - <div data-slot="session-review-container" class={props.classes?.container}> <Show when={hasDiffs()} fallback={props.empty}> <div class="pb-6"> @@ -627,8 +284,7 @@ export const SessionReview = (props: SessionReviewProps) => { {(file) => { let wrapper: HTMLDivElement | undefined - const diff = createMemo(() => diffs().get(file)) - const item = () => diff()! + const item = createMemo(() => diffs().get(file)!) const expanded = createMemo(() => open().includes(file)) const force = () => !!store.force[file] @@ -720,9 +376,6 @@ export const SessionReview = (props: SessionReviewProps) => { onCleanup(() => { anchors.delete(file) - readyFiles.delete(file) - searchHandles.delete(file) - if (highlightedFile === file) highlightedFile = undefined }) const handleLineSelected = (range: SelectedLineRange | null) => { @@ -839,9 +492,7 @@ export const SessionReview = (props: SessionReviewProps) => { mode="diff" preloadedDiff={item().preloaded} diffStyle={diffStyle()} - expansionLineCount={searchExpanded() ? Number.MAX_SAFE_INTEGER : 20} onRendered={() => { - readyFiles.add(file) props.onDiffRendered?.() }} enableLineSelection={props.onLineComment != null} @@ -854,21 +505,6 @@ export const SessionReview = (props: SessionReviewProps) => { renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined} selectedLines={selectedLines()} commentedLines={commentedLines()} - search={{ - shortcuts: "disabled", - showBar: false, - disableVirtualization: searchExpanded(), - register: (handle: FileSearchHandle | null) => { - if (!handle) { - searchHandles.delete(file) - readyFiles.delete(file) - if (highlightedFile === file) highlightedFile = undefined - return - } - - searchHandles.set(file, handle) - }, - }} before={{ name: file, contents: typeof item().before === "string" ? item().before : "", |
