Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions packages/app/src/pages/session/file-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"
import { sampledChecksum } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
import { showToast } from "@opencode-ai/ui/toast"
import { LineComment as LineCommentView, LineCommentEditor } from "@opencode-ai/ui/line-comment"
Expand Down Expand Up @@ -49,7 +49,7 @@ export function FileTabContent(props: {
return props.file.get(p)
})
const contents = createMemo(() => state()?.content?.content ?? "")
const cacheKey = createMemo(() => checksum(contents()))
const cacheKey = createMemo(() => sampledChecksum(contents()))
const isImage = createMemo(() => {
const c = state()?.content
return c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml"
Expand Down Expand Up @@ -163,11 +163,20 @@ export function FileTabContent(props: {
return
}

const estimateTop = (range: SelectedLineRange) => {
const line = Math.max(range.start, range.end)
const height = 24
const offset = 2
return Math.max(0, (line - 1) * height + offset)
}

const large = contents().length > 500_000

const next: Record<string, number> = {}
for (const comment of fileComments()) {
const marker = findMarker(root, comment.selection)
if (!marker) continue
next[comment.id] = markerTop(el, marker)
if (marker) next[comment.id] = markerTop(el, marker)
else if (large) next[comment.id] = estimateTop(comment.selection)
}

const removed = Object.keys(note.positions).filter((id) => next[id] === undefined)
Expand All @@ -194,12 +203,12 @@ export function FileTabContent(props: {
}

const marker = findMarker(root, range)
if (!marker) {
setNote("draftTop", undefined)
if (marker) {
setNote("draftTop", markerTop(el, marker))
return
}

setNote("draftTop", markerTop(el, marker))
setNote("draftTop", large ? estimateTop(range) : undefined)
}

const scheduleComments = () => {
Expand Down
118 changes: 93 additions & 25 deletions packages/ui/src/components/code.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
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> & {
Expand Down Expand Up @@ -160,16 +177,28 @@ export function Code<T>(props: CodeProps<T>) {

const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 })

const file = createMemo(
() =>
new File<T>(
{
...createDefaultOptions<T>("unified"),
...others,
},
getWorkerPool("unified"),
),
)
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")
Expand Down Expand Up @@ -577,27 +606,35 @@ export function Code<T>(props: CodeProps<T>) {
}

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) {
file().setSelectedLines(null)
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) {
file().setSelectedLines(null)
current.setSelectedLines(null)
return true
}

if (!root.querySelector(`[data-line="${start}"]`) || !root.querySelector(`[data-line="${end}"]`)) {
file().setSelectedLines(null)
current.setSelectedLines(null)
return true
}

Expand All @@ -608,7 +645,7 @@ export function Code<T>(props: CodeProps<T>) {
return { start: range.start, end: range.end }
})()

file().setSelectedLines(normalized)
current.setSelectedLines(normalized)
return true
}

Expand All @@ -619,9 +656,12 @@ export function Code<T>(props: CodeProps<T>) {

const token = renderToken

const lines = lineCount()
const lines = virtual() ? undefined : lineCount()

const isReady = (root: ShadowRoot) => root.querySelectorAll("[data-line]").length >= lines
const isReady = (root: ShadowRoot) =>
virtual()
? root.querySelector("[data-line]") != null
: root.querySelectorAll("[data-line]").length >= (lines ?? 0)

const notify = () => {
if (token !== renderToken) return
Expand Down Expand Up @@ -844,20 +884,41 @@ export function Code<T>(props: CodeProps<T>) {
}

createEffect(() => {
const current = file()
const opts = options()
const workerPool = getWorkerPool("unified")
const isVirtual = virtual()

onCleanup(() => {
current.cleanUp()
})
})

createEffect(() => {
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()
file().render({
instance.render({
file: typeof local.file.contents === "string" ? local.file : { ...local.file, contents: value },
lineAnnotations: local.annotations,
containerWrapper: container,
Expand Down Expand Up @@ -910,6 +971,13 @@ export function Code<T>(props: CodeProps<T>) {
onCleanup(() => {
observer?.disconnect()

instance?.cleanUp()
instance = undefined

virtualizer?.cleanUp()
virtualizer = undefined
virtualRoot = undefined

clearOverlayScroll()
clearOverlay()
if (findCurrent === host) {
Expand Down
38 changes: 29 additions & 9 deletions packages/ui/src/components/diff.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { checksum } from "@opencode-ai/util/encode"
import { FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import { sampledChecksum } from "@opencode-ai/util/encode"
import { FileDiff, type FileDiffOptions, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
import { createMediaQuery } from "@solid-primitives/media"
import { createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
Expand Down Expand Up @@ -78,14 +78,29 @@ export function Diff<T>(props: DiffProps<T>) {

const mobile = createMediaQuery("(max-width: 640px)")

const options = createMemo(() => {
const opts = {
const large = createMemo(() => {
const before = typeof local.before?.contents === "string" ? local.before.contents : ""
const after = typeof local.after?.contents === "string" ? local.after.contents : ""
return Math.max(before.length, after.length) > 500_000
})

const largeOptions = {
lineDiffType: "none",
maxLineDiffLength: 0,
tokenizeMaxLineLength: 1,
} satisfies Pick<FileDiffOptions<T>, "lineDiffType" | "maxLineDiffLength" | "tokenizeMaxLineLength">

const options = createMemo<FileDiffOptions<T>>(() => {
const base = {
...createDefaultOptions(props.diffStyle),
...others,
}
if (!mobile()) return opts

const perf = large() ? { ...base, ...largeOptions } : base
if (!mobile()) return perf

return {
...opts,
...perf,
disableLineNumbers: true,
}
})
Expand Down Expand Up @@ -528,12 +543,17 @@ export function Diff<T>(props: DiffProps<T>) {

createEffect(() => {
const opts = options()
const workerPool = getWorkerPool(props.diffStyle)
const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use unified pool for large diffs - it skips expensive word-level highlighting

const virtualizer = getVirtualizer()
const annotations = local.annotations
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""

const cacheKey = (contents: string) => {
if (!large()) return sampledChecksum(contents, contents.length)
return sampledChecksum(contents)
}

instance?.cleanUp()
instance = virtualizer
? new VirtualizedFileDiff<T>(opts, virtualizer, virtualMetrics, workerPool)
Expand All @@ -545,12 +565,12 @@ export function Diff<T>(props: DiffProps<T>) {
oldFile: {
...local.before,
contents: beforeContents,
cacheKey: checksum(beforeContents),
cacheKey: cacheKey(beforeContents),
},
newFile: {
...local.after,
contents: afterContents,
cacheKey: checksum(afterContents),
cacheKey: cacheKey(afterContents),
},
lineAnnotations: annotations,
containerWrapper: container,
Expand Down
26 changes: 26 additions & 0 deletions packages/ui/src/components/session-review.css
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,30 @@
--line-comment-popover-z: 30;
--line-comment-open-z: 6;
}

[data-slot="session-review-large-diff"] {
padding: 12px;
background: var(--background-stronger);
}

[data-slot="session-review-large-diff-title"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
color: var(--text-strong);
margin-bottom: 4px;
}

[data-slot="session-review-large-diff-meta"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
color: var(--text-weak);
word-break: break-word;
}

[data-slot="session-review-large-diff-actions"] {
display: flex;
gap: 8px;
margin-top: 10px;
}
}
Loading
Loading