diff options
Diffstat (limited to 'packages/ui/src/components/code.tsx')
| -rw-r--r-- | packages/ui/src/components/code.tsx | 1097 |
1 files changed, 0 insertions, 1097 deletions
diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx deleted file mode 100644 index 837cc5337..000000000 --- a/packages/ui/src/components/code.tsx +++ /dev/null @@ -1,1097 +0,0 @@ -import { - DEFAULT_VIRTUAL_FILE_METRICS, - type FileContents, - File, - FileOptions, - LineAnnotation, - type SelectedLineRange, - type VirtualFileMetrics, - VirtualizedFile, - Virtualizer, -} 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" - -const VIRTUALIZE_BYTES = 500_000 -const codeMetrics = { - ...DEFAULT_VIRTUAL_FILE_METRICS, - lineHeight: 24, - fileGap: 0, -} satisfies Partial<VirtualFileMetrics> - -type SelectionSide = "additions" | "deletions" - -export type CodeProps<T = {}> = FileOptions<T> & { - file: FileContents - annotations?: LineAnnotation<T>[] - selectedLines?: SelectedLineRange | null - commentedLines?: SelectedLineRange[] - onRendered?: () => void - onLineSelectionEnd?: (selection: SelectedLineRange | null) => void - class?: string - classList?: ComponentProps<"div">["classList"] -} - -function findElement(node: Node | null): HTMLElement | undefined { - if (!node) return - if (node instanceof HTMLElement) return node - return node.parentElement ?? undefined -} - -function findLineNumber(node: Node | null): number | undefined { - const element = findElement(node) - if (!element) return - - const line = element.closest("[data-line]") - if (!(line instanceof HTMLElement)) return - - const value = parseInt(line.dataset.line ?? "", 10) - if (Number.isNaN(value)) return - - return value -} - -function findSide(node: Node | null): SelectionSide | undefined { - const element = findElement(node) - if (!element) return - - const code = element.closest("[data-code]") - if (!(code instanceof HTMLElement)) return - - if (code.hasAttribute("data-deletions")) return "deletions" - return "additions" -} - -type FindHost = { - element: () => HTMLElement | undefined - open: () => void - close: () => void - next: (dir: 1 | -1) => void - isOpen: () => boolean -} - -const findHosts = new Set<FindHost>() -let findTarget: FindHost | undefined -let findCurrent: FindHost | undefined -let findInstalled = false - -function isEditable(node: unknown): boolean { - if (!(node instanceof HTMLElement)) return false - if (node.closest("[data-prevent-autofocus]")) return true - if (node.isContentEditable) return true - return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(node.tagName) -} - -function hostForNode(node: unknown): FindHost | undefined { - if (!(node instanceof Node)) return - for (const host of findHosts) { - const el = host.element() - if (el && el.isConnected && el.contains(node)) return host - } -} - -function installFindShortcuts() { - if (findInstalled) return - if (typeof window === "undefined") return - findInstalled = true - - window.addEventListener( - "keydown", - (event) => { - if (event.defaultPrevented) return - - const mod = event.metaKey || event.ctrlKey - if (!mod) return - - const key = event.key.toLowerCase() - - if (key === "g") { - const host = findCurrent - if (!host || !host.isOpen()) return - event.preventDefault() - event.stopPropagation() - host.next(event.shiftKey ? -1 : 1) - return - } - - if (key !== "f") return - - const current = findCurrent - if (current && current.isOpen()) { - event.preventDefault() - event.stopPropagation() - current.open() - return - } - - const host = - hostForNode(document.activeElement) ?? hostForNode(event.target) ?? findTarget ?? Array.from(findHosts)[0] - if (!host) return - - event.preventDefault() - event.stopPropagation() - host.open() - }, - { capture: true }, - ) -} - -export function Code<T>(props: CodeProps<T>) { - let wrapper!: HTMLDivElement - let container!: HTMLDivElement - let findInput: HTMLInputElement | undefined - let findOverlay!: HTMLDivElement - let findOverlayFrame: number | undefined - let findOverlayScroll: HTMLElement[] = [] - let observer: MutationObserver | undefined - let renderToken = 0 - let selectionFrame: number | undefined - let dragFrame: number | undefined - let dragStart: number | undefined - let dragEnd: number | undefined - let dragMoved = false - let lastSelection: SelectedLineRange | null = null - let pendingSelectionEnd = false - - const [local, others] = splitProps(props, [ - "file", - "class", - "classList", - "annotations", - "selectedLines", - "commentedLines", - "onRendered", - ]) - - const [rendered, setRendered] = createSignal(0) - - const [findOpen, setFindOpen] = createSignal(false) - const [findQuery, setFindQuery] = createSignal("") - const [findIndex, setFindIndex] = createSignal(0) - const [findCount, setFindCount] = createSignal(0) - let findMode: "highlights" | "overlay" = "overlay" - let findHits: Range[] = [] - - const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 }) - - let instance: File<T> | VirtualizedFile<T> | undefined - let virtualizer: Virtualizer | undefined - let virtualRoot: Document | HTMLElement | undefined - - const bytes = createMemo(() => { - const value = local.file.contents as unknown - if (typeof value === "string") return value.length - if (Array.isArray(value)) { - return value.reduce( - (acc, part) => acc + (typeof part === "string" ? part.length + 1 : String(part).length + 1), - 0, - ) - } - if (value == null) return 0 - return String(value).length - }) - const virtual = createMemo(() => bytes() > VIRTUALIZE_BYTES) - - const options = createMemo(() => ({ - ...createDefaultOptions<T>("unified"), - ...others, - })) - - const getRoot = () => { - const host = container.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - - const root = host.shadowRoot - if (!root) return - - return root - } - - const applyScheme = () => { - const host = container.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - - const scheme = document.documentElement.dataset.colorScheme - if (scheme === "dark" || scheme === "light") { - host.dataset.colorScheme = scheme - return - } - - host.removeAttribute("data-color-scheme") - } - - const supportsHighlights = () => { - const g = globalThis as unknown as { CSS?: { highlights?: unknown }; Highlight?: unknown } - return typeof g.Highlight === "function" && g.CSS?.highlights != null - } - - const clearHighlightFind = () => { - const api = (globalThis as { CSS?: { highlights?: { delete: (name: string) => void } } }).CSS?.highlights - if (!api) return - api.delete("opencode-find") - api.delete("opencode-find-current") - } - - const clearOverlayScroll = () => { - for (const el of findOverlayScroll) el.removeEventListener("scroll", scheduleOverlay) - findOverlayScroll = [] - } - - const clearOverlay = () => { - if (findOverlayFrame !== undefined) { - cancelAnimationFrame(findOverlayFrame) - findOverlayFrame = undefined - } - findOverlay.innerHTML = "" - } - - const renderOverlay = () => { - if (findMode !== "overlay") { - clearOverlay() - return - } - - clearOverlay() - if (findHits.length === 0) return - - const base = wrapper.getBoundingClientRect() - const current = findIndex() - - const frag = document.createDocumentFragment() - for (let i = 0; i < findHits.length; i++) { - const range = findHits[i] - const active = i === current - - for (const rect of Array.from(range.getClientRects())) { - if (!rect.width || !rect.height) continue - - const el = document.createElement("div") - el.style.position = "absolute" - el.style.left = `${Math.round(rect.left - base.left)}px` - el.style.top = `${Math.round(rect.top - base.top)}px` - el.style.width = `${Math.round(rect.width)}px` - el.style.height = `${Math.round(rect.height)}px` - el.style.borderRadius = "2px" - el.style.backgroundColor = active ? "var(--surface-warning-strong)" : "var(--surface-warning-base)" - el.style.opacity = active ? "0.55" : "0.35" - if (active) el.style.boxShadow = "inset 0 0 0 1px var(--border-warning-base)" - frag.appendChild(el) - } - } - - findOverlay.appendChild(frag) - } - - function scheduleOverlay() { - if (findMode !== "overlay") return - if (!findOpen()) return - if (findOverlayFrame !== undefined) return - - findOverlayFrame = requestAnimationFrame(() => { - findOverlayFrame = undefined - renderOverlay() - }) - } - - const syncOverlayScroll = () => { - if (findMode !== "overlay") return - const root = getRoot() - - const next = root - ? Array.from(root.querySelectorAll("[data-code]")).filter( - (node): node is HTMLElement => node instanceof HTMLElement, - ) - : [] - if (next.length === findOverlayScroll.length && next.every((el, i) => el === findOverlayScroll[i])) return - - clearOverlayScroll() - findOverlayScroll = next - for (const el of findOverlayScroll) el.addEventListener("scroll", scheduleOverlay, { passive: true }) - } - - const clearFind = () => { - clearHighlightFind() - clearOverlay() - clearOverlayScroll() - findHits = [] - setFindCount(0) - setFindIndex(0) - } - - 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 - } - } - - const positionFindBar = () => { - 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) => { - const needle = query.toLowerCase() - const out: Range[] = [] - - const cols = Array.from(root.querySelectorAll("[data-content] [data-line], [data-column-content]")).filter( - (node): node is HTMLElement => node instanceof HTMLElement, - ) - - for (const col of cols) { - const text = col.textContent - if (!text) continue - - const hay = text.toLowerCase() - let idx = hay.indexOf(needle) - if (idx === -1) continue - - const nodes: Text[] = [] - const ends: number[] = [] - const walker = document.createTreeWalker(col, NodeFilter.SHOW_TEXT) - let node = walker.nextNode() - let pos = 0 - - while (node) { - if (node instanceof Text) { - pos += node.data.length - nodes.push(node) - ends.push(pos) - } - node = walker.nextNode() - } - - if (nodes.length === 0) continue - - const locate = (at: number) => { - let lo = 0 - let hi = ends.length - 1 - while (lo < hi) { - const mid = (lo + hi) >> 1 - if (ends[mid] >= at) hi = mid - else lo = mid + 1 - } - const prev = lo === 0 ? 0 : ends[lo - 1] - return { node: nodes[lo], offset: at - prev } - } - - while (idx !== -1) { - const start = locate(idx) - const end = locate(idx + query.length) - const range = document.createRange() - range.setStart(start.node, start.offset) - range.setEnd(end.node, end.offset) - out.push(range) - idx = hay.indexOf(needle, idx + query.length) - } - } - - return out - } - - const scrollToRange = (range: Range) => { - const start = range.startContainer - const el = start instanceof Element ? start : start.parentElement - el?.scrollIntoView({ block: "center", inline: "center" }) - } - - const setHighlights = (ranges: Range[], index: number) => { - const api = (globalThis as unknown as { CSS?: { highlights?: any }; Highlight?: any }).CSS?.highlights - const Highlight = (globalThis as unknown as { Highlight?: any }).Highlight - if (!api || typeof Highlight !== "function") return false - - api.delete("opencode-find") - api.delete("opencode-find-current") - - const active = ranges[index] - if (active) api.set("opencode-find-current", new Highlight(active)) - - const rest = ranges.filter((_, i) => i !== index) - if (rest.length > 0) api.set("opencode-find", new Highlight(...rest)) - return true - } - - const applyFind = (opts?: { reset?: boolean; scroll?: boolean }) => { - if (!findOpen()) return - - const query = findQuery().trim() - if (!query) { - clearFind() - return - } - - const root = getRoot() - if (!root) return - - findMode = supportsHighlights() ? "highlights" : "overlay" - - const ranges = scanFind(root, query) - const total = ranges.length - const desired = opts?.reset ? 0 : findIndex() - const index = total ? Math.min(desired, total - 1) : 0 - - findHits = ranges - setFindCount(total) - setFindIndex(index) - - const active = ranges[index] - if (findMode === "highlights") { - clearOverlay() - clearOverlayScroll() - if (!setHighlights(ranges, index)) { - findMode = "overlay" - clearHighlightFind() - syncOverlayScroll() - scheduleOverlay() - } - if (opts?.scroll && active) { - scrollToRange(active) - } - return - } - - clearHighlightFind() - syncOverlayScroll() - if (opts?.scroll && active) { - scrollToRange(active) - } - scheduleOverlay() - } - - const closeFind = () => { - setFindOpen(false) - clearFind() - if (findCurrent === host) findCurrent = undefined - } - - const stepFind = (dir: 1 | -1) => { - if (!findOpen()) return - const total = findCount() - if (total <= 0) return - - const index = (findIndex() + dir + total) % total - setFindIndex(index) - - const active = findHits[index] - if (!active) return - - if (findMode === "highlights") { - if (!setHighlights(findHits, index)) { - findMode = "overlay" - applyFind({ reset: true, scroll: true }) - return - } - scrollToRange(active) - return - } - - clearHighlightFind() - syncOverlayScroll() - scrollToRange(active) - scheduleOverlay() - } - - const host: FindHost = { - element: () => wrapper, - isOpen: () => findOpen(), - next: stepFind, - open: () => { - if (findCurrent && findCurrent !== host) findCurrent.close() - findCurrent = host - findTarget = host - - if (!findOpen()) setFindOpen(true) - requestAnimationFrame(() => { - applyFind({ scroll: true }) - findInput?.focus() - findInput?.select() - }) - }, - close: closeFind, - } - - onMount(() => { - findMode = supportsHighlights() ? "highlights" : "overlay" - installFindShortcuts() - findHosts.add(host) - if (!findTarget) findTarget = host - - onCleanup(() => { - findHosts.delete(host) - if (findCurrent === host) { - findCurrent = undefined - clearHighlightFind() - } - if (findTarget === host) findTarget = undefined - }) - }) - - createEffect(() => { - if (!findOpen()) return - - 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(() => { - window.removeEventListener("resize", update) - observer?.disconnect() - }) - }) - - const applyCommentedLines = (ranges: SelectedLineRange[]) => { - const root = getRoot() - if (!root) return - - const existing = Array.from(root.querySelectorAll("[data-comment-selected]")) - for (const node of existing) { - if (!(node instanceof HTMLElement)) continue - node.removeAttribute("data-comment-selected") - } - - const annotations = Array.from(root.querySelectorAll("[data-line-annotation]")).filter( - (node): node is HTMLElement => node instanceof HTMLElement, - ) - - for (const range of ranges) { - const start = Math.max(1, Math.min(range.start, range.end)) - const end = Math.max(range.start, range.end) - - for (let line = start; line <= end; line++) { - const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-column-number="${line}"]`)) - for (const node of nodes) { - if (!(node instanceof HTMLElement)) continue - node.setAttribute("data-comment-selected", "") - } - } - - for (const annotation of annotations) { - const line = parseInt(annotation.dataset.lineAnnotation?.split(",")[1] ?? "", 10) - if (Number.isNaN(line)) continue - if (line < start || line > end) continue - annotation.setAttribute("data-comment-selected", "") - } - } - } - - const text = () => { - const value = local.file.contents as unknown - if (typeof value === "string") return value - if (Array.isArray(value)) return value.join("\n") - if (value == null) return "" - return String(value) - } - - const lineCount = () => { - const value = text() - const total = value.split("\n").length - (value.endsWith("\n") ? 1 : 0) - return Math.max(1, total) - } - - const applySelection = (range: SelectedLineRange | null) => { - const current = instance - if (!current) return false - - if (virtual()) { - current.setSelectedLines(range) - return true - } - - const root = getRoot() - if (!root) return false - - const lines = lineCount() - if (root.querySelectorAll("[data-line]").length < lines) return false - - if (!range) { - current.setSelectedLines(null) - return true - } - - const start = Math.min(range.start, range.end) - const end = Math.max(range.start, range.end) - - if (start < 1 || end > lines) { - current.setSelectedLines(null) - return true - } - - if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) { - current.setSelectedLines(null) - return true - } - - const normalized = (() => { - if (range.endSide != null) return { start: range.start, end: range.end } - if (range.side !== "deletions") return range - if (root.querySelector("[data-deletions]") != null) return range - return { start: range.start, end: range.end } - })() - - current.setSelectedLines(normalized) - return true - } - - const notifyRendered = () => { - observer?.disconnect() - observer = undefined - renderToken++ - - const token = renderToken - - const lines = virtual() ? undefined : lineCount() - - const isReady = (root: ShadowRoot) => - virtual() - ? root.querySelector("[data-line]") != null - : root.querySelectorAll("[data-line]").length >= (lines ?? 0) - - const notify = () => { - if (token !== renderToken) return - - observer?.disconnect() - observer = undefined - requestAnimationFrame(() => { - if (token !== renderToken) return - applySelection(lastSelection) - applyFind({ reset: true }) - local.onRendered?.() - }) - } - - const root = getRoot() - if (root && isReady(root)) { - notify() - return - } - - if (typeof MutationObserver === "undefined") return - - const observeRoot = (root: ShadowRoot) => { - if (isReady(root)) { - notify() - return - } - - observer?.disconnect() - observer = new MutationObserver(() => { - if (token !== renderToken) return - if (!isReady(root)) return - - notify() - }) - - observer.observe(root, { childList: true, subtree: true }) - } - - if (root) { - observeRoot(root) - return - } - - observer = new MutationObserver(() => { - if (token !== renderToken) return - - const root = getRoot() - if (!root) return - - observeRoot(root) - }) - - observer.observe(container, { childList: true, subtree: true }) - } - - const updateSelection = () => { - const root = getRoot() - if (!root) return - - const selection = - (root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection() - if (!selection || selection.isCollapsed) return - - const domRange = - ( - selection as unknown as { - getComposedRanges?: (options?: { shadowRoots?: ShadowRoot[] }) => Range[] - } - ).getComposedRanges?.({ shadowRoots: [root] })?.[0] ?? - (selection.rangeCount > 0 ? selection.getRangeAt(0) : undefined) - - const startNode = domRange?.startContainer ?? selection.anchorNode - const endNode = domRange?.endContainer ?? selection.focusNode - if (!startNode || !endNode) return - - if (!root.contains(startNode) || !root.contains(endNode)) return - - const start = findLineNumber(startNode) - const end = findLineNumber(endNode) - if (start === undefined || end === undefined) return - - const startSide = findSide(startNode) - const endSide = findSide(endNode) - const side = startSide ?? endSide - - const selected: SelectedLineRange = { - start, - end, - } - - if (side) selected.side = side - if (endSide && side && endSide !== side) selected.endSide = endSide - - setSelectedLines(selected) - } - - const setSelectedLines = (range: SelectedLineRange | null) => { - lastSelection = range - applySelection(range) - } - - const scheduleSelectionUpdate = () => { - if (selectionFrame !== undefined) return - - selectionFrame = requestAnimationFrame(() => { - selectionFrame = undefined - updateSelection() - - if (!pendingSelectionEnd) return - pendingSelectionEnd = false - props.onLineSelectionEnd?.(lastSelection) - }) - } - - const updateDragSelection = () => { - if (dragStart === undefined || dragEnd === undefined) return - - const start = Math.min(dragStart, dragEnd) - const end = Math.max(dragStart, dragEnd) - - setSelectedLines({ start, end }) - } - - const scheduleDragUpdate = () => { - if (dragFrame !== undefined) return - - dragFrame = requestAnimationFrame(() => { - dragFrame = undefined - updateDragSelection() - }) - } - - const lineFromMouseEvent = (event: MouseEvent) => { - const path = event.composedPath() - - let numberColumn = false - let line: number | undefined - - for (const item of path) { - if (!(item instanceof HTMLElement)) continue - - numberColumn = numberColumn || item.dataset.columnNumber != null - - if (line === undefined && item.dataset.line) { - const parsed = parseInt(item.dataset.line, 10) - if (!Number.isNaN(parsed)) line = parsed - } - - if (numberColumn && line !== undefined) break - } - - return { line, numberColumn } - } - - const handleMouseDown = (event: MouseEvent) => { - if (props.enableLineSelection !== true) return - if (event.button !== 0) return - - const { line, numberColumn } = lineFromMouseEvent(event) - if (numberColumn) return - if (line === undefined) return - - dragStart = line - dragEnd = line - dragMoved = false - } - - const handleMouseMove = (event: MouseEvent) => { - if (props.enableLineSelection !== true) return - if (dragStart === undefined) return - - if ((event.buttons & 1) === 0) { - dragStart = undefined - dragEnd = undefined - dragMoved = false - return - } - - const { line } = lineFromMouseEvent(event) - if (line === undefined) return - - dragEnd = line - dragMoved = true - scheduleDragUpdate() - } - - const handleMouseUp = () => { - if (props.enableLineSelection !== true) return - if (dragStart === undefined) return - - if (!dragMoved) { - pendingSelectionEnd = false - const line = dragStart - setSelectedLines({ start: line, end: line }) - props.onLineSelectionEnd?.(lastSelection) - dragStart = undefined - dragEnd = undefined - dragMoved = false - return - } - - pendingSelectionEnd = true - scheduleDragUpdate() - scheduleSelectionUpdate() - - dragStart = undefined - dragEnd = undefined - dragMoved = false - } - - const handleSelectionChange = () => { - if (props.enableLineSelection !== true) return - if (dragStart === undefined) return - - const selection = window.getSelection() - if (!selection || selection.isCollapsed) return - - scheduleSelectionUpdate() - } - - createEffect(() => { - const opts = options() - const workerPool = getWorkerPool("unified") - const isVirtual = virtual() - - observer?.disconnect() - observer = undefined - - instance?.cleanUp() - instance = undefined - - if (!isVirtual && virtualizer) { - virtualizer.cleanUp() - virtualizer = undefined - virtualRoot = undefined - } - - const v = (() => { - if (!isVirtual) return - if (typeof document === "undefined") return - - const root = getScrollParent(wrapper) ?? document - if (virtualizer && virtualRoot === root) return virtualizer - - virtualizer?.cleanUp() - virtualizer = new Virtualizer() - virtualRoot = root - virtualizer.setup(root, root instanceof Document ? undefined : wrapper) - return virtualizer - })() - - instance = isVirtual && v ? new VirtualizedFile<T>(opts, v, codeMetrics, workerPool) : new File<T>(opts, workerPool) - - container.innerHTML = "" - const value = text() - instance.render({ - file: typeof local.file.contents === "string" ? local.file : { ...local.file, contents: value }, - lineAnnotations: local.annotations, - containerWrapper: container, - }) - - applyScheme() - - setRendered((value) => value + 1) - notifyRendered() - }) - - createEffect(() => { - if (typeof document === "undefined") return - if (typeof MutationObserver === "undefined") return - - const root = document.documentElement - const monitor = new MutationObserver(() => applyScheme()) - monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) - applyScheme() - - onCleanup(() => monitor.disconnect()) - }) - - createEffect(() => { - rendered() - const ranges = local.commentedLines ?? [] - requestAnimationFrame(() => applyCommentedLines(ranges)) - }) - - createEffect(() => { - setSelectedLines(local.selectedLines ?? null) - }) - - createEffect(() => { - if (props.enableLineSelection !== true) return - - container.addEventListener("mousedown", handleMouseDown) - container.addEventListener("mousemove", handleMouseMove) - window.addEventListener("mouseup", handleMouseUp) - document.addEventListener("selectionchange", handleSelectionChange) - - onCleanup(() => { - container.removeEventListener("mousedown", handleMouseDown) - container.removeEventListener("mousemove", handleMouseMove) - window.removeEventListener("mouseup", handleMouseUp) - document.removeEventListener("selectionchange", handleSelectionChange) - }) - }) - - onCleanup(() => { - observer?.disconnect() - - instance?.cleanUp() - instance = undefined - - virtualizer?.cleanUp() - virtualizer = undefined - virtualRoot = undefined - - clearOverlayScroll() - clearOverlay() - if (findCurrent === host) { - findCurrent = undefined - clearHighlightFind() - } - - if (selectionFrame !== undefined) { - cancelAnimationFrame(selectionFrame) - selectionFrame = undefined - } - - if (dragFrame !== undefined) { - cancelAnimationFrame(dragFrame) - dragFrame = undefined - } - - dragStart = undefined - dragEnd = undefined - dragMoved = false - lastSelection = null - 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" - style={styleVariables} - class="relative outline-none" - classList={{ - ...(local.classList || {}), - [local.class ?? ""]: !!local.class, - }} - ref={wrapper} - tabIndex={0} - onPointerDown={() => { - findTarget = host - wrapper.focus({ preventScroll: true }) - }} - onFocus={() => { - findTarget = host - }} - > - <Show when={findOpen()}> - <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`, - }} - /> - </Portal> - </Show> - <div ref={container} /> - <div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" /> - </div> - ) -} |
