فهرست منبع

wip(app): line selection

Adam 1 ماه پیش
والد
کامیت
640d1f1ecc

+ 30 - 13
packages/app/src/components/prompt-input.tsx

@@ -15,7 +15,7 @@ import {
 import { createStore, produce } from "solid-js/store"
 import { createStore, produce } from "solid-js/store"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
 import { useLocal } from "@/context/local"
-import { useFile, type FileSelection } from "@/context/file"
+import { selectionFromLines, useFile, type FileSelection } from "@/context/file"
 import {
 import {
   ContentPart,
   ContentPart,
   DEFAULT_PROMPT,
   DEFAULT_PROMPT,
@@ -163,6 +163,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     if (!tab) return
     if (!tab) return
     return files.pathFromTab(tab)
     return files.pathFromTab(tab)
   })
   })
+
+  const activeFileSelection = createMemo(() => {
+    const path = activeFile()
+    if (!path) return
+    const range = files.selectedLines(path)
+    if (!range) return
+    return selectionFromLines(range)
+  })
   const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
   const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
   const status = createMemo(
   const status = createMemo(
     () =>
     () =>
@@ -1256,7 +1264,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
 
     const activePath = activeFile()
     const activePath = activeFile()
     if (activePath && prompt.context.activeTab()) {
     if (activePath && prompt.context.activeTab()) {
-      addContextFile(activePath)
+      addContextFile(activePath, activeFileSelection())
     }
     }
 
 
     for (const item of prompt.context.items()) {
     for (const item of prompt.context.items()) {
@@ -1476,22 +1484,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             </div>
             </div>
           </div>
           </div>
         </Show>
         </Show>
-        <Show when={false && (prompt.context.items().length > 0 || !!activeFile())}>
-          <div class="flex flex-wrap items-center gap-2 px-3 pt-3">
+        <Show when={prompt.context.items().length > 0 || !!activeFile()}>
+          <div class="flex flex-wrap items-center gap-1.5 px-3 pt-3">
             <Show when={prompt.context.activeTab() ? activeFile() : undefined}>
             <Show when={prompt.context.activeTab() ? activeFile() : undefined}>
               {(path) => (
               {(path) => (
-                <div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
-                  <FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-4" />
-                  <div class="flex items-center text-12-regular min-w-0">
+                <div class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base max-w-full">
+                  <FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-3.5" />
+                  <div class="flex items-center text-11-regular min-w-0">
                     <span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
                     <span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
                     <span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
                     <span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
+                    <Show when={activeFileSelection()}>
+                      {(sel) => (
+                        <span class="text-text-weak whitespace-nowrap ml-1">
+                          {sel().startLine === sel().endLine
+                            ? `:${sel().startLine}`
+                            : `:${sel().startLine}-${sel().endLine}`}
+                        </span>
+                      )}
+                    </Show>
                     <span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
                     <span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
                   </div>
                   </div>
                   <IconButton
                   <IconButton
                     type="button"
                     type="button"
                     icon="close"
                     icon="close"
                     variant="ghost"
                     variant="ghost"
-                    class="h-6 w-6"
+                    class="h-5 w-5"
                     onClick={() => prompt.context.removeActive()}
                     onClick={() => prompt.context.removeActive()}
                     aria-label={language.t("prompt.context.removeActiveFile")}
                     aria-label={language.t("prompt.context.removeActiveFile")}
                   />
                   />
@@ -1501,7 +1518,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <Show when={!prompt.context.activeTab() && !!activeFile()}>
             <Show when={!prompt.context.activeTab() && !!activeFile()}>
               <button
               <button
                 type="button"
                 type="button"
-                class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base text-12-regular text-text-weak hover:bg-surface-raised-base-hover"
+                class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base text-11-regular text-text-weak hover:bg-surface-raised-base-hover"
                 onClick={() => prompt.context.addActive()}
                 onClick={() => prompt.context.addActive()}
               >
               >
                 <Icon name="plus-small" size="small" />
                 <Icon name="plus-small" size="small" />
@@ -1510,9 +1527,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             </Show>
             </Show>
             <For each={prompt.context.items()}>
             <For each={prompt.context.items()}>
               {(item) => (
               {(item) => (
-                <div class="flex items-center gap-2 px-2 py-1 rounded-md bg-surface-base border border-border-base max-w-full">
-                  <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
-                  <div class="flex items-center text-12-regular min-w-0">
+                <div class="flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base max-w-full">
+                  <FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
+                  <div class="flex items-center text-11-regular min-w-0">
                     <span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
                     <span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(item.path)}</span>
                     <span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
                     <span class="text-text-strong whitespace-nowrap">{getFilename(item.path)}</span>
                     <Show when={item.selection}>
                     <Show when={item.selection}>
@@ -1529,7 +1546,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     type="button"
                     type="button"
                     icon="close"
                     icon="close"
                     variant="ghost"
                     variant="ghost"
-                    class="h-6 w-6"
+                    class="h-5 w-5"
                     onClick={() => prompt.context.remove(item.key)}
                     onClick={() => prompt.context.remove(item.key)}
                     aria-label={language.t("prompt.context.removeFile")}
                     aria-label={language.t("prompt.context.removeFile")}
                   />
                   />

+ 4 - 8
packages/app/src/components/session/session-context-tab.tsx

@@ -282,7 +282,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
       }
       }
     })
     })
 
 
-    return <Code file={file()} overflow="wrap" class="select-text" />
+    return (
+      <Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
+    )
   }
   }
 
 
   function RawMessage(msgProps: { message: Message }) {
   function RawMessage(msgProps: { message: Message }) {
@@ -314,19 +316,13 @@ export function SessionContextTab(props: SessionContextTabProps) {
   let frame: number | undefined
   let frame: number | undefined
   let pending: { x: number; y: number } | undefined
   let pending: { x: number; y: number } | undefined
 
 
-  const restoreScroll = (retries = 0) => {
+  const restoreScroll = () => {
     const el = scroll
     const el = scroll
     if (!el) return
     if (!el) return
 
 
     const s = props.view()?.scroll("context")
     const s = props.view()?.scroll("context")
     if (!s) return
     if (!s) return
 
 
-    // Wait for content to be scrollable - content may not have rendered yet
-    if (el.scrollHeight <= el.clientHeight && retries < 10) {
-      requestAnimationFrame(() => restoreScroll(retries + 1))
-      return
-    }
-
     if (el.scrollTop !== s.y) el.scrollTop = s.y
     if (el.scrollTop !== s.y) el.scrollTop = s.y
     if (el.scrollLeft !== s.x) el.scrollLeft = s.x
     if (el.scrollLeft !== s.x) el.scrollLeft = s.x
   }
   }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 383 - 209
packages/app/src/pages/session.tsx


+ 254 - 18
packages/ui/src/components/code.tsx

@@ -9,6 +9,7 @@ export type CodeProps<T = {}> = FileOptions<T> & {
   file: FileContents
   file: FileContents
   annotations?: LineAnnotation<T>[]
   annotations?: LineAnnotation<T>[]
   selectedLines?: SelectedLineRange | null
   selectedLines?: SelectedLineRange | null
+  onRendered?: () => void
   class?: string
   class?: string
   classList?: ComponentProps<"div">["classList"]
   classList?: ComponentProps<"div">["classList"]
 }
 }
@@ -45,8 +46,32 @@ function findSide(node: Node | null): SelectionSide | undefined {
 
 
 export function Code<T>(props: CodeProps<T>) {
 export function Code<T>(props: CodeProps<T>) {
   let container!: HTMLDivElement
   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(
   const file = createMemo(
     () =>
     () =>
@@ -54,6 +79,7 @@ export function Code<T>(props: CodeProps<T>) {
         {
         {
           ...createDefaultOptions<T>("unified"),
           ...createDefaultOptions<T>("unified"),
           ...others,
           ...others,
+          onLineClick: props.enableLineSelection === true || props.onLineClick ? handleLineClick : undefined,
         },
         },
         getWorkerPool("unified"),
         getWorkerPool("unified"),
       ),
       ),
@@ -69,37 +95,218 @@ export function Code<T>(props: CodeProps<T>) {
     return root
     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()
     const root = getRoot()
     if (!root) return
     if (!root) return
 
 
-    const selection = window.getSelection()
+    const selection =
+      (root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
     if (!selection || selection.isCollapsed) return
     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
     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 side = startSide ?? endSide
 
 
-    const range: SelectedLineRange = {
+    const selected: SelectedLineRange = {
       start,
       start,
       end,
       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(() => {
   createEffect(() => {
@@ -111,12 +318,17 @@ export function Code<T>(props: CodeProps<T>) {
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
+    observer?.disconnect()
+    observer = undefined
+
     container.innerHTML = ""
     container.innerHTML = ""
     file().render({
     file().render({
       file: local.file,
       file: local.file,
       lineAnnotations: local.annotations,
       lineAnnotations: local.annotations,
       containerWrapper: container,
       containerWrapper: container,
     })
     })
+
+    notifyRendered()
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
@@ -126,13 +338,37 @@ export function Code<T>(props: CodeProps<T>) {
   createEffect(() => {
   createEffect(() => {
     if (props.enableLineSelection !== true) return
     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(() => {
     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 (
   return (
     <div
     <div
       data-component="code"
       data-component="code"

+ 96 - 1
packages/ui/src/components/diff.tsx

@@ -7,7 +7,10 @@ import { getWorkerPool } from "../pierre/worker"
 
 
 export function Diff<T>(props: DiffProps<T>) {
 export function Diff<T>(props: DiffProps<T>) {
   let container!: HTMLDivElement
   let container!: HTMLDivElement
-  const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
+  let observer: MutationObserver | undefined
+  let renderToken = 0
+
+  const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations", "onRendered"])
 
 
   const mobile = createMediaQuery("(max-width: 640px)")
   const mobile = createMediaQuery("(max-width: 640px)")
 
 
@@ -25,6 +28,95 @@ export function Diff<T>(props: DiffProps<T>) {
 
 
   let instance: FileDiff<T> | undefined
   let instance: FileDiff<T> | undefined
 
 
+  const getRoot = () => {
+    const host = container.querySelector("diffs-container")
+    if (!(host instanceof HTMLElement)) return
+
+    const root = host.shadowRoot
+    if (!root) return
+
+    return root
+  }
+
+  const notifyRendered = () => {
+    if (!local.onRendered) return
+
+    observer?.disconnect()
+    observer = undefined
+    renderToken++
+
+    const token = renderToken
+    let settle = 0
+
+    const isReady = (root: ShadowRoot) => root.querySelector("[data-line]") != null
+
+    const notify = () => {
+      if (token !== renderToken) return
+
+      observer?.disconnect()
+      observer = undefined
+      requestAnimationFrame(() => {
+        if (token !== renderToken) return
+        local.onRendered?.()
+      })
+    }
+
+    const schedule = () => {
+      settle++
+      const current = settle
+
+      requestAnimationFrame(() => {
+        if (token !== renderToken) return
+        if (current !== settle) return
+
+        requestAnimationFrame(() => {
+          if (token !== renderToken) return
+          if (current !== settle) return
+
+          notify()
+        })
+      })
+    }
+
+    const observeRoot = (root: ShadowRoot) => {
+      observer?.disconnect()
+      observer = new MutationObserver(() => {
+        if (token !== renderToken) return
+        if (!isReady(root)) return
+
+        schedule()
+      })
+
+      observer.observe(root, { childList: true, subtree: true })
+
+      if (!isReady(root)) return
+      schedule()
+    }
+
+    const root = getRoot()
+    if (typeof MutationObserver === "undefined") {
+      if (!root || !isReady(root)) return
+      local.onRendered()
+      return
+    }
+
+    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 })
+  }
+
   createEffect(() => {
   createEffect(() => {
     const opts = options()
     const opts = options()
     const workerPool = getWorkerPool(props.diffStyle)
     const workerPool = getWorkerPool(props.diffStyle)
@@ -50,9 +142,12 @@ export function Diff<T>(props: DiffProps<T>) {
       lineAnnotations: annotations,
       lineAnnotations: annotations,
       containerWrapper: container,
       containerWrapper: container,
     })
     })
+
+    notifyRendered()
   })
   })
 
 
   onCleanup(() => {
   onCleanup(() => {
+    observer?.disconnect()
     instance?.cleanUp()
     instance?.cleanUp()
   })
   })
 
 

+ 2 - 0
packages/ui/src/components/session-review.tsx

@@ -22,6 +22,7 @@ export interface SessionReviewProps {
   split?: boolean
   split?: boolean
   diffStyle?: SessionReviewDiffStyle
   diffStyle?: SessionReviewDiffStyle
   onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
   onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
+  onDiffRendered?: () => void
   open?: string[]
   open?: string[]
   onOpenChange?: (open: string[]) => void
   onOpenChange?: (open: string[]) => void
   scrollRef?: (el: HTMLDivElement) => void
   scrollRef?: (el: HTMLDivElement) => void
@@ -346,6 +347,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                           component={diffComponent}
                           component={diffComponent}
                           preloadedDiff={diff.preloaded}
                           preloadedDiff={diff.preloaded}
                           diffStyle={diffStyle()}
                           diffStyle={diffStyle()}
+                          onRendered={props.onDiffRendered}
                           before={{
                           before={{
                             name: diff.file!,
                             name: diff.file!,
                             contents: beforeText(),
                             contents: beforeText(),

+ 15 - 6
packages/ui/src/pierre/index.ts

@@ -5,6 +5,7 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & {
   before: FileContents
   before: FileContents
   after: FileContents
   after: FileContents
   annotations?: DiffLineAnnotation<T>[]
   annotations?: DiffLineAnnotation<T>[]
+  onRendered?: () => void
   class?: string
   class?: string
   classList?: ComponentProps<"div">["classList"]
   classList?: ComponentProps<"div">["classList"]
 }
 }
@@ -18,9 +19,9 @@ const unsafeCSS = `
   --diffs-bg-separator: var(--diffs-bg-separator-override, light-dark( color-mix(in lab, var(--diffs-bg) 96%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-mixer))));
   --diffs-bg-separator: var(--diffs-bg-separator-override, light-dark( color-mix(in lab, var(--diffs-bg) 96%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-mixer))));
   --diffs-fg: light-dark(var(--diffs-light), var(--diffs-dark));
   --diffs-fg: light-dark(var(--diffs-light), var(--diffs-dark));
   --diffs-fg-number: var(--diffs-fg-number-override, light-dark(color-mix(in lab, var(--diffs-fg) 65%, var(--diffs-bg)), color-mix(in lab, var(--diffs-fg) 65%, var(--diffs-bg))));
   --diffs-fg-number: var(--diffs-fg-number-override, light-dark(color-mix(in lab, var(--diffs-fg) 65%, var(--diffs-bg)), color-mix(in lab, var(--diffs-fg) 65%, var(--diffs-bg))));
-  --diffs-deletion-base: var(--diffs-deletion-color-override, light-dark(var(--diffs-light-deletion-color, var(--diffs-deletion-color, rgb(255, 0, 0))), var(--diffs-dark-deletion-color, var(--diffs-deletion-color, rgb(255, 0, 0)))));
-  --diffs-addition-base: var(--diffs-addition-color-override, light-dark(var(--diffs-light-addition-color, var(--diffs-addition-color, rgb(0, 255, 0))), var(--diffs-dark-addition-color, var(--diffs-addition-color, rgb(0, 255, 0)))));
-  --diffs-modified-base: var(--diffs-modified-color-override, light-dark(var(--diffs-light-modified-color, var(--diffs-modified-color, rgb(0, 0, 255))), var(--diffs-dark-modified-color, var(--diffs-modified-color, rgb(0, 0, 255)))));
+  --diffs-deletion-base: var(--syntax-diff-delete);
+  --diffs-addition-base: var(--syntax-diff-add);
+  --diffs-modified-base: var(--syntax-diff-unknown);
   --diffs-bg-deletion: var(--diffs-bg-deletion-override, light-dark( color-mix(in lab, var(--diffs-bg) 98%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 92%, var(--diffs-deletion-base))));
   --diffs-bg-deletion: var(--diffs-bg-deletion-override, light-dark( color-mix(in lab, var(--diffs-bg) 98%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 92%, var(--diffs-deletion-base))));
   --diffs-bg-deletion-number: var(--diffs-bg-deletion-number-override, light-dark( color-mix(in lab, var(--diffs-bg) 91%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-deletion-base))));
   --diffs-bg-deletion-number: var(--diffs-bg-deletion-number-override, light-dark( color-mix(in lab, var(--diffs-bg) 91%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-deletion-base))));
   --diffs-bg-deletion-hover: var(--diffs-bg-deletion-hover-override, light-dark( color-mix(in lab, var(--diffs-bg) 80%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 75%, var(--diffs-deletion-base))));
   --diffs-bg-deletion-hover: var(--diffs-bg-deletion-hover-override, light-dark( color-mix(in lab, var(--diffs-bg) 80%, var(--diffs-deletion-base)), color-mix(in lab, var(--diffs-bg) 75%, var(--diffs-deletion-base))));
@@ -29,10 +30,15 @@ const unsafeCSS = `
   --diffs-bg-addition-number: var(--diffs-bg-addition-number-override, light-dark( color-mix(in lab, var(--diffs-bg) 91%, var(--diffs-addition-base)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-addition-base))));
   --diffs-bg-addition-number: var(--diffs-bg-addition-number-override, light-dark( color-mix(in lab, var(--diffs-bg) 91%, var(--diffs-addition-base)), color-mix(in lab, var(--diffs-bg) 85%, var(--diffs-addition-base))));
   --diffs-bg-addition-hover: var(--diffs-bg-addition-hover-override, light-dark( color-mix(in lab, var(--diffs-bg) 80%, var(--diffs-addition-base)), color-mix(in lab, var(--diffs-bg) 70%, var(--diffs-addition-base))));
   --diffs-bg-addition-hover: var(--diffs-bg-addition-hover-override, light-dark( color-mix(in lab, var(--diffs-bg) 80%, var(--diffs-addition-base)), color-mix(in lab, var(--diffs-bg) 70%, var(--diffs-addition-base))));
   --diffs-bg-addition-emphasis: var(--diffs-bg-addition-emphasis-override, light-dark(rgb(from var(--diffs-addition-base) r g b / 0.07), rgb(from var(--diffs-addition-base) r g b / 0.1)));
   --diffs-bg-addition-emphasis: var(--diffs-bg-addition-emphasis-override, light-dark(rgb(from var(--diffs-addition-base) r g b / 0.07), rgb(from var(--diffs-addition-base) r g b / 0.1)));
-  --diffs-selection-base: var(--diffs-modified-base);
+  --diffs-selection-base: var(--text-interactive-base);
   --diffs-selection-number-fg: light-dark( color-mix(in lab, var(--diffs-selection-base) 65%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-selection-base) 75%, var(--diffs-mixer)));
   --diffs-selection-number-fg: light-dark( color-mix(in lab, var(--diffs-selection-base) 65%, var(--diffs-mixer)), color-mix(in lab, var(--diffs-selection-base) 75%, var(--diffs-mixer)));
-  --diffs-bg-selection: var(--diffs-bg-selection-override, light-dark( color-mix(in lab, var(--diffs-bg) 82%, var(--diffs-selection-base)), color-mix(in lab, var(--diffs-bg) 75%, var(--diffs-selection-base))));
-  --diffs-bg-selection-number: var(--diffs-bg-selection-number-override, light-dark( color-mix(in lab, var(--diffs-bg) 75%, var(--diffs-selection-base)), color-mix(in lab, var(--diffs-bg) 60%, var(--diffs-selection-base))));
+  --diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--diffs-selection-base) r g b / 0.18));
+  --diffs-bg-selection-number: var(--diffs-bg-selection-number-override, rgb(from var(--diffs-selection-base) r g b / 0.22));
+  --diffs-bg-selection-text: rgb(from var(--diffs-selection-base) r g b / 0.12);
+}
+
+[data-diffs] ::selection {
+  background-color: var(--diffs-bg-selection-text);
 }
 }
 
 
 [data-diffs-header],
 [data-diffs-header],
@@ -57,6 +63,9 @@ const unsafeCSS = `
   [data-separator-content] {
   [data-separator-content] {
     height: 24px !important;
     height: 24px !important;
   }
   }
+  [data-column-number] {
+    background-color: var(--background-stronger);
+  }
   [data-code] {
   [data-code] {
     overflow-x: auto !important;
     overflow-x: auto !important;
   }
   }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است