| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 |
- 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"
- 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 })
- const file = createMemo(
- () =>
- new File<T>(
- {
- ...createDefaultOptions<T>("unified"),
- ...others,
- },
- getWorkerPool("unified"),
- ),
- )
- 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 root = getRoot()
- if (!root) return false
- const lines = lineCount()
- if (root.querySelectorAll("[data-line]").length < lines) return false
- if (!range) {
- file().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) {
- file().setSelectedLines(null)
- return true
- }
- if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) {
- file().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 }
- })()
- file().setSelectedLines(normalized)
- return true
- }
- const notifyRendered = () => {
- observer?.disconnect()
- observer = undefined
- renderToken++
- const token = renderToken
- const lines = lineCount()
- const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines
- 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 current = file()
- onCleanup(() => {
- current.cleanUp()
- })
- })
- createEffect(() => {
- observer?.disconnect()
- observer = undefined
- container.innerHTML = ""
- const value = text()
- file().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()
- 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>
- )
- }
|