From 640d1f1ecc7a2b46fb2bafed760c7348c70579a8 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Sun, 4 Jan 2026 15:40:25 -0600
Subject: wip(app): line selection

---
 packages/ui/src/components/code.tsx | 272 +++++++++++++++++++++++++++++++++---
 1 file changed, 254 insertions(+), 18 deletions(-)

(limited to 'packages/ui/src/components/code.tsx')

diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx
index ed7db368c..c6f702fb5 100644
--- a/packages/ui/src/components/code.tsx
+++ b/packages/ui/src/components/code.tsx
@@ -9,6 +9,7 @@ export type CodeProps<T = {}> = FileOptions<T> & {
   file: FileContents
   annotations?: LineAnnotation<T>[]
   selectedLines?: SelectedLineRange | null
+  onRendered?: () => void
   class?: string
   classList?: ComponentProps<"div">["classList"]
 }
@@ -45,8 +46,32 @@ function findSide(node: Node | null): SelectionSide | undefined {
 
 export function Code<T>(props: CodeProps<T>) {
   let container!: HTMLDivElement
+  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
+
+  const [local, others] = splitProps(props, [
+    "file",
+    "class",
+    "classList",
+    "annotations",
+    "selectedLines",
+    "onRendered",
+  ])
+
+  const handleLineClick: FileOptions<T>["onLineClick"] = (info) => {
+    props.onLineClick?.(info)
 
-  const [local, others] = splitProps(props, ["file", "class", "classList", "annotations", "selectedLines"])
+    if (props.enableLineSelection !== true) return
+    if (info.numberColumn) return
+    if (!local.selectedLines) return
+
+    file().setSelectedLines(null)
+  }
 
   const file = createMemo(
     () =>
@@ -54,6 +79,7 @@ export function Code<T>(props: CodeProps<T>) {
         {
           ...createDefaultOptions<T>("unified"),
           ...others,
+          onLineClick: props.enableLineSelection === true || props.onLineClick ? handleLineClick : undefined,
         },
         getWorkerPool("unified"),
       ),
@@ -69,37 +95,218 @@ export function Code<T>(props: CodeProps<T>) {
     return root
   }
 
-  const handleMouseUp = () => {
-    if (props.enableLineSelection !== true) return
+  const notifyRendered = () => {
+    if (!local.onRendered) return
+
+    observer?.disconnect()
+    observer = undefined
+    renderToken++
+
+    const token = renderToken
+
+    const lines = (() => {
+      const text = local.file.contents
+      const total = text.split("\n").length - (text.endsWith("\n") ? 1 : 0)
+      return Math.max(1, total)
+    })()
+
+    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
+        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 = window.getSelection()
+    const selection =
+      (root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
     if (!selection || selection.isCollapsed) return
 
-    const anchor = selection.anchorNode
-    const focus = selection.focusNode
-    if (!anchor || !focus) return
-    if (!root.contains(anchor) || !root.contains(focus)) 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
 
-    const start = findLineNumber(anchor)
-    const end = findLineNumber(focus)
+    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(anchor)
-    const endSide = findSide(focus)
+    const startSide = findSide(startNode)
+    const endSide = findSide(endNode)
     const side = startSide ?? endSide
 
-    const range: SelectedLineRange = {
+    const selected: SelectedLineRange = {
       start,
       end,
     }
 
-    if (side) range.side = side
-    if (endSide && side && endSide !== side) range.endSide = endSide
+    if (side) selected.side = side
+    if (endSide && side && endSide !== side) selected.endSide = endSide
+
+    file().setSelectedLines(selected)
+  }
+
+  const scheduleSelectionUpdate = () => {
+    if (selectionFrame !== undefined) return
+
+    selectionFrame = requestAnimationFrame(() => {
+      selectionFrame = undefined
+      updateSelection()
+    })
+  }
+
+  const updateDragSelection = () => {
+    if (dragStart === undefined || dragEnd === undefined) return
+
+    const start = Math.min(dragStart, dragEnd)
+    const end = Math.max(dragStart, dragEnd)
+
+    file().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) {
+      if (dragMoved) scheduleDragUpdate()
+      dragStart = undefined
+      dragEnd = undefined
+      dragMoved = false
+    }
+
+    scheduleSelectionUpdate()
+  }
+
+  const handleSelectionChange = () => {
+    if (props.enableLineSelection !== true) return
+
+    const selection = window.getSelection()
+    if (!selection || selection.isCollapsed) return
 
-    file().setSelectedLines(range)
+    scheduleSelectionUpdate()
   }
 
   createEffect(() => {
@@ -111,12 +318,17 @@ export function Code<T>(props: CodeProps<T>) {
   })
 
   createEffect(() => {
+    observer?.disconnect()
+    observer = undefined
+
     container.innerHTML = ""
     file().render({
       file: local.file,
       lineAnnotations: local.annotations,
       containerWrapper: container,
     })
+
+    notifyRendered()
   })
 
   createEffect(() => {
@@ -126,13 +338,37 @@ export function Code<T>(props: CodeProps<T>) {
   createEffect(() => {
     if (props.enableLineSelection !== true) return
 
-    container.addEventListener("mouseup", handleMouseUp)
+    container.addEventListener("mousedown", handleMouseDown)
+    container.addEventListener("mousemove", handleMouseMove)
+    window.addEventListener("mouseup", handleMouseUp)
+    document.addEventListener("selectionchange", handleSelectionChange)
 
     onCleanup(() => {
-      container.removeEventListener("mouseup", handleMouseUp)
+      container.removeEventListener("mousedown", handleMouseDown)
+      container.removeEventListener("mousemove", handleMouseMove)
+      window.removeEventListener("mouseup", handleMouseUp)
+      document.removeEventListener("selectionchange", handleSelectionChange)
     })
   })
 
+  onCleanup(() => {
+    observer?.disconnect()
+
+    if (selectionFrame !== undefined) {
+      cancelAnimationFrame(selectionFrame)
+      selectionFrame = undefined
+    }
+
+    if (dragFrame !== undefined) {
+      cancelAnimationFrame(dragFrame)
+      dragFrame = undefined
+    }
+
+    dragStart = undefined
+    dragEnd = undefined
+    dragMoved = false
+  })
+
   return (
     <div
       data-component="code"
-- 
cgit v1.2.3

