diff options
| author | Adam <[email protected]> | 2026-02-10 14:01:14 -0600 |
|---|---|---|
| committer | opencode <[email protected]> | 2026-02-10 20:22:31 +0000 |
| commit | 55119559b320535253e67925d27ecad2466d50ed (patch) | |
| tree | 1313c15a272774c0b41516194d241f9427edd39f /packages/ui/src/components/code.tsx | |
| parent | fd5531316f858b77274e26975796aec41ba5128c (diff) | |
| download | opencode-55119559b320535253e67925d27ecad2466d50ed.tar.gz opencode-55119559b320535253e67925d27ecad2466d50ed.zip | |
fix(app): don't scroll code search input
Diffstat (limited to 'packages/ui/src/components/code.tsx')
| -rw-r--r-- | packages/ui/src/components/code.tsx | 170 |
1 files changed, 88 insertions, 82 deletions
diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index e3e1e5652..4e7c82d78 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -1,5 +1,6 @@ import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" +import { Portal } from "solid-js/web" import { createDefaultOptions, styleVariables } from "../pierre" import { getWorkerPool } from "../pierre/worker" import { Icon } from "./icon" @@ -125,11 +126,9 @@ export function Code<T>(props: CodeProps<T>) { let wrapper!: HTMLDivElement let container!: HTMLDivElement let findInput: HTMLInputElement | undefined - let findBar: HTMLDivElement | undefined let findOverlay!: HTMLDivElement let findOverlayFrame: number | undefined let findOverlayScroll: HTMLElement[] = [] - let findScroll: HTMLElement | undefined let observer: MutationObserver | undefined let renderToken = 0 let selectionFrame: number | undefined @@ -159,6 +158,8 @@ export function Code<T>(props: CodeProps<T>) { let findMode: "highlights" | "overlay" = "overlay" let findHits: Range[] = [] + const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 }) + const file = createMemo( () => new File<T>( @@ -291,23 +292,26 @@ export function Code<T>(props: CodeProps<T>) { setFindIndex(0) } - const getScrollParent = (el: HTMLElement): HTMLElement | null => { + const getScrollParent = (el: HTMLElement): HTMLElement | undefined => { let parent = el.parentElement while (parent) { const style = getComputedStyle(parent) if (style.overflowY === "auto" || style.overflowY === "scroll") return parent parent = parent.parentElement } - return null } const positionFindBar = () => { - if (!findBar || !wrapper) return - const scrollTop = findScroll ? findScroll.scrollTop : window.scrollY - findBar.style.position = "absolute" - findBar.style.top = `${scrollTop + 8}px` - findBar.style.right = "8px" - findBar.style.left = "" + if (typeof window === "undefined") return + + const root = getScrollParent(wrapper) ?? wrapper + const rect = root.getBoundingClientRect() + const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height")) + const header = Number.isNaN(title) ? 0 : title + setFindPos({ + top: Math.round(rect.top) + header - 4, + right: Math.round(window.innerWidth - rect.right) + 8, + }) } const scanFind = (root: ShadowRoot, query: string) => { @@ -426,7 +430,6 @@ export function Code<T>(props: CodeProps<T>) { } if (opts?.scroll && active) { scrollToRange(active) - positionFindBar() } return } @@ -435,7 +438,6 @@ export function Code<T>(props: CodeProps<T>) { syncOverlayScroll() if (opts?.scroll && active) { scrollToRange(active) - positionFindBar() } scheduleOverlay() } @@ -464,14 +466,12 @@ export function Code<T>(props: CodeProps<T>) { return } scrollToRange(active) - positionFindBar() return } clearHighlightFind() syncOverlayScroll() scrollToRange(active) - positionFindBar() scheduleOverlay() } @@ -484,11 +484,9 @@ export function Code<T>(props: CodeProps<T>) { findCurrent = host findTarget = host - findScroll = getScrollParent(wrapper) ?? undefined if (!findOpen()) setFindOpen(true) requestAnimationFrame(() => { applyFind({ scroll: true }) - positionFindBar() findInput?.focus() findInput?.select() }) @@ -514,18 +512,18 @@ export function Code<T>(props: CodeProps<T>) { createEffect(() => { if (!findOpen()) return - findScroll = getScrollParent(wrapper) ?? undefined - const target = findScroll ?? window - const handler = () => positionFindBar() - target.addEventListener("scroll", handler, { passive: true }) - window.addEventListener("resize", handler, { passive: true }) - handler() + const update = () => positionFindBar() + requestAnimationFrame(update) + window.addEventListener("resize", update, { passive: true }) + + const root = getScrollParent(wrapper) ?? wrapper + const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update()) + observer?.observe(root) onCleanup(() => { - target.removeEventListener("scroll", handler) - window.removeEventListener("resize", handler) - findScroll = undefined + window.removeEventListener("resize", update) + observer?.disconnect() }) }) @@ -916,6 +914,64 @@ export function Code<T>(props: CodeProps<T>) { pendingSelectionEnd = false }) + const FindBar = (barProps: { class: string; style?: ComponentProps<"div">["style"] }) => ( + <div class={barProps.class} style={barProps.style} onPointerDown={(e) => e.stopPropagation()}> + <Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" /> + <input + ref={findInput} + placeholder="Find" + value={findQuery()} + class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak" + onInput={(e) => { + setFindQuery(e.currentTarget.value) + setFindIndex(0) + applyFind({ reset: true, scroll: true }) + }} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.preventDefault() + closeFind() + return + } + if (e.key !== "Enter") return + e.preventDefault() + stepFind(e.shiftKey ? -1 : 1) + }} + /> + <div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}> + {findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"} + </div> + <div class="flex items-center"> + <button + type="button" + class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" + disabled={findCount() === 0} + aria-label="Previous match" + onClick={() => stepFind(-1)} + > + <Icon name="chevron-down" size="small" class="rotate-180" /> + </button> + <button + type="button" + class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" + disabled={findCount() === 0} + aria-label="Next match" + onClick={() => stepFind(1)} + > + <Icon name="chevron-down" size="small" /> + </button> + </div> + <button + type="button" + class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong" + aria-label="Close search" + onClick={closeFind} + > + <Icon name="close-small" size="small" /> + </button> + </div> + ) + return ( <div data-component="code" @@ -936,65 +992,15 @@ export function Code<T>(props: CodeProps<T>) { }} > <Show when={findOpen()}> - <div - ref={findBar} - class="z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md" - onPointerDown={(e) => e.stopPropagation()} - > - <Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" /> - <input - ref={findInput} - placeholder="Find" - value={findQuery()} - class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak" - onInput={(e) => { - setFindQuery(e.currentTarget.value) - setFindIndex(0) - applyFind({ reset: true, scroll: true }) - }} - onKeyDown={(e) => { - if (e.key === "Escape") { - e.preventDefault() - closeFind() - return - } - if (e.key !== "Enter") return - e.preventDefault() - stepFind(e.shiftKey ? -1 : 1) + <Portal> + <FindBar + class="fixed z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md" + style={{ + top: `${findPos().top}px`, + right: `${findPos().right}px`, }} /> - <div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}> - {findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"} - </div> - <div class="flex items-center"> - <button - type="button" - class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" - disabled={findCount() === 0} - aria-label="Previous match" - onClick={() => stepFind(-1)} - > - <Icon name="chevron-down" size="small" class="rotate-180" /> - </button> - <button - type="button" - class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none" - disabled={findCount() === 0} - aria-label="Next match" - onClick={() => stepFind(1)} - > - <Icon name="chevron-down" size="small" /> - </button> - </div> - <button - type="button" - class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong" - aria-label="Close search" - onClick={closeFind} - > - <Icon name="close-small" size="small" /> - </button> - </div> + </Portal> </Show> <div ref={container} /> <div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" /> |
