| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097 |
- 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>
- )
- }
|