summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/code.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-10 14:01:14 -0600
committeropencode <[email protected]>2026-02-10 20:22:31 +0000
commit55119559b320535253e67925d27ecad2466d50ed (patch)
tree1313c15a272774c0b41516194d241f9427edd39f /packages/ui/src/components/code.tsx
parentfd5531316f858b77274e26975796aec41ba5128c (diff)
downloadopencode-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.tsx170
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" />