fix(desktop): performance optimization for showing large diff & files (#13460)
This commit is contained in:
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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> & {
|
||||
@@ -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")
|
||||
@@ -577,6 +606,14 @@ 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
|
||||
|
||||
@@ -584,7 +621,7 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
if (root.querySelectorAll("[data-line]").length < lines) return false
|
||||
|
||||
if (!range) {
|
||||
file().setSelectedLines(null)
|
||||
current.setSelectedLines(null)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -592,12 +629,12 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,26 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { type SelectedLineRange } from "@pierre/diffs"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
|
||||
const MAX_DIFF_LINES = 20_000
|
||||
const MAX_DIFF_BYTES = 2_000_000
|
||||
|
||||
function linesOver(text: string, max: number) {
|
||||
let lines = 1
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text.charCodeAt(i) !== 10) continue
|
||||
lines++
|
||||
if (lines > max) return true
|
||||
}
|
||||
return lines > max
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${Math.round((bytes / 1024) * 10) / 10} KB`
|
||||
return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MB`
|
||||
}
|
||||
|
||||
export type SessionReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export type SessionReviewComment = {
|
||||
@@ -326,12 +346,28 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
{(diff) => {
|
||||
let wrapper: HTMLDivElement | undefined
|
||||
|
||||
const expanded = createMemo(() => open().includes(diff.file))
|
||||
const [force, setForce] = createSignal(false)
|
||||
|
||||
const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
|
||||
const commentedLines = createMemo(() => comments().map((c) => c.selection))
|
||||
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
|
||||
const tooLarge = createMemo(() => {
|
||||
if (!expanded()) return false
|
||||
if (force()) return false
|
||||
if (isImageFile(diff.file)) return false
|
||||
|
||||
const before = beforeText()
|
||||
const after = afterText()
|
||||
|
||||
if (before.length > MAX_DIFF_BYTES || after.length > MAX_DIFF_BYTES) return true
|
||||
if (linesOver(before, MAX_DIFF_LINES) || linesOver(after, MAX_DIFF_LINES)) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
@@ -571,94 +607,114 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading" ? "Loading..." : "Image"}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<LineComment
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
open={isCommentOpen(comment)}
|
||||
comment={comment.comment}
|
||||
selection={selectionLabel(comment.selection)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={draftTop()}
|
||||
value={draft()}
|
||||
selection={selectionLabel(range())}
|
||||
onInput={setDraft}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
<Show when={expanded()}>
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading"
|
||||
? i18n.t("ui.sessionReview.image.loading")
|
||||
: i18n.t("ui.sessionReview.image.placeholder")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage() && tooLarge()}>
|
||||
<div data-slot="session-review-large-diff">
|
||||
<div data-slot="session-review-large-diff-title">
|
||||
{i18n.t("ui.sessionReview.largeDiff.title")}
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-meta">
|
||||
Limit: {MAX_DIFF_LINES.toLocaleString()} lines / {formatBytes(MAX_DIFF_BYTES)}.
|
||||
Current: {formatBytes(Math.max(beforeText().length, afterText().length))}.
|
||||
</div>
|
||||
<div data-slot="session-review-large-diff-actions">
|
||||
<Button size="normal" variant="secondary" onClick={() => setForce(true)}>
|
||||
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
<LineComment
|
||||
id={comment.id}
|
||||
top={positions()[comment.id]}
|
||||
onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
|
||||
onClick={() => {
|
||||
if (isCommentOpen(comment)) {
|
||||
setOpened(null)
|
||||
return
|
||||
}
|
||||
|
||||
openComment(comment)
|
||||
}}
|
||||
open={isCommentOpen(comment)}
|
||||
comment={comment.comment}
|
||||
selection={selectionLabel(comment.selection)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={draftRange()}>
|
||||
{(range) => (
|
||||
<Show when={draftTop() !== undefined}>
|
||||
<LineCommentEditor
|
||||
top={draftTop()}
|
||||
value={draft()}
|
||||
selection={selectionLabel(range())}
|
||||
onInput={setDraft}
|
||||
onCancel={() => setCommenting(null)}
|
||||
onSubmit={(comment) => {
|
||||
props.onLineComment?.({
|
||||
file: diff.file,
|
||||
selection: range(),
|
||||
comment,
|
||||
preview: selectionPreview(diff, range()),
|
||||
})
|
||||
setCommenting(null)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "مضاف",
|
||||
"ui.sessionReview.change.removed": "محذوف",
|
||||
"ui.sessionReview.change.modified": "معدل",
|
||||
"ui.sessionReview.image.loading": "جار التحميل...",
|
||||
"ui.sessionReview.image.placeholder": "صورة",
|
||||
"ui.sessionReview.largeDiff.title": "Diff كبير جدا لعرضه",
|
||||
"ui.sessionReview.largeDiff.meta": "الحد: {{lines}} سطر / {{limit}}. الحالي: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "اعرض على أي حال",
|
||||
|
||||
"ui.lineComment.label.prefix": "تعليق على ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Adicionado",
|
||||
"ui.sessionReview.change.removed": "Removido",
|
||||
"ui.sessionReview.change.modified": "Modificado",
|
||||
"ui.sessionReview.image.loading": "Carregando...",
|
||||
"ui.sessionReview.image.placeholder": "Imagem",
|
||||
"ui.sessionReview.largeDiff.title": "Diff grande demais para renderizar",
|
||||
"ui.sessionReview.largeDiff.meta": "Limite: {{lines}} linhas / {{limit}}. Atual: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Renderizar mesmo assim",
|
||||
|
||||
"ui.lineComment.label.prefix": "Comentar em ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -12,6 +12,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Dodano",
|
||||
"ui.sessionReview.change.removed": "Uklonjeno",
|
||||
"ui.sessionReview.change.modified": "Izmijenjeno",
|
||||
"ui.sessionReview.image.loading": "Učitavanje...",
|
||||
"ui.sessionReview.image.placeholder": "Slika",
|
||||
"ui.sessionReview.largeDiff.title": "Diff je prevelik za prikaz",
|
||||
"ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linija / {{limit}}. Trenutno: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Prikaži svejedno",
|
||||
|
||||
"ui.lineComment.label.prefix": "Komentar na ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -9,6 +9,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Tilføjet",
|
||||
"ui.sessionReview.change.removed": "Fjernet",
|
||||
"ui.sessionReview.change.modified": "Ændret",
|
||||
"ui.sessionReview.image.loading": "Indlæser...",
|
||||
"ui.sessionReview.image.placeholder": "Billede",
|
||||
"ui.sessionReview.largeDiff.title": "Diff er for stor til at blive vist",
|
||||
"ui.sessionReview.largeDiff.meta": "Grænse: {{lines}} linjer / {{limit}}. Nuværende: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Vis alligevel",
|
||||
"ui.lineComment.label.prefix": "Kommenter på ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Kommenterer på ",
|
||||
|
||||
@@ -13,6 +13,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Hinzugefügt",
|
||||
"ui.sessionReview.change.removed": "Entfernt",
|
||||
"ui.sessionReview.change.modified": "Geändert",
|
||||
"ui.sessionReview.image.loading": "Wird geladen...",
|
||||
"ui.sessionReview.image.placeholder": "Bild",
|
||||
"ui.sessionReview.largeDiff.title": "Diff zu groß zum Rendern",
|
||||
"ui.sessionReview.largeDiff.meta": "Limit: {{lines}} Zeilen / {{limit}}. Aktuell: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Trotzdem rendern",
|
||||
"ui.lineComment.label.prefix": "Kommentar zu ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Kommentiere ",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Added",
|
||||
"ui.sessionReview.change.removed": "Removed",
|
||||
"ui.sessionReview.change.modified": "Modified",
|
||||
"ui.sessionReview.image.loading": "Loading...",
|
||||
"ui.sessionReview.image.placeholder": "Image",
|
||||
"ui.sessionReview.largeDiff.title": "Diff too large to render",
|
||||
"ui.sessionReview.largeDiff.meta": "Limit: {{lines}} lines / {{limit}}. Current: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Render anyway",
|
||||
|
||||
"ui.lineComment.label.prefix": "Comment on ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Añadido",
|
||||
"ui.sessionReview.change.removed": "Eliminado",
|
||||
"ui.sessionReview.change.modified": "Modificado",
|
||||
"ui.sessionReview.image.loading": "Cargando...",
|
||||
"ui.sessionReview.image.placeholder": "Imagen",
|
||||
"ui.sessionReview.largeDiff.title": "Diff demasiado grande para renderizar",
|
||||
"ui.sessionReview.largeDiff.meta": "Límite: {{lines}} líneas / {{limit}}. Actual: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Renderizar de todos modos",
|
||||
|
||||
"ui.lineComment.label.prefix": "Comentar en ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Ajouté",
|
||||
"ui.sessionReview.change.removed": "Supprimé",
|
||||
"ui.sessionReview.change.modified": "Modifié",
|
||||
"ui.sessionReview.image.loading": "Chargement...",
|
||||
"ui.sessionReview.image.placeholder": "Image",
|
||||
"ui.sessionReview.largeDiff.title": "Diff trop volumineux pour être affiché",
|
||||
"ui.sessionReview.largeDiff.meta": "Limite : {{lines}} lignes / {{limit}}. Actuel : {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Afficher quand même",
|
||||
|
||||
"ui.lineComment.label.prefix": "Commenter sur ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -9,6 +9,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "追加",
|
||||
"ui.sessionReview.change.removed": "削除",
|
||||
"ui.sessionReview.change.modified": "変更",
|
||||
"ui.sessionReview.image.loading": "読み込み中...",
|
||||
"ui.sessionReview.image.placeholder": "画像",
|
||||
"ui.sessionReview.largeDiff.title": "差分が大きすぎて表示できません",
|
||||
"ui.sessionReview.largeDiff.meta": "上限: {{lines}} 行 / {{limit}}。現在: {{current}}。",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "それでも表示する",
|
||||
"ui.lineComment.label.prefix": "",
|
||||
"ui.lineComment.label.suffix": "へのコメント",
|
||||
"ui.lineComment.editorLabel.prefix": "",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "추가됨",
|
||||
"ui.sessionReview.change.removed": "삭제됨",
|
||||
"ui.sessionReview.change.modified": "수정됨",
|
||||
"ui.sessionReview.image.loading": "로딩 중...",
|
||||
"ui.sessionReview.image.placeholder": "이미지",
|
||||
"ui.sessionReview.largeDiff.title": "차이가 너무 커서 렌더링할 수 없습니다",
|
||||
"ui.sessionReview.largeDiff.meta": "제한: {{lines}}줄 / {{limit}}. 현재: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "그래도 렌더링",
|
||||
|
||||
"ui.lineComment.label.prefix": "",
|
||||
"ui.lineComment.label.suffix": "에 댓글 달기",
|
||||
|
||||
@@ -11,6 +11,11 @@ export const dict: Record<Keys, string> = {
|
||||
"ui.sessionReview.change.added": "Lagt til",
|
||||
"ui.sessionReview.change.removed": "Fjernet",
|
||||
"ui.sessionReview.change.modified": "Endret",
|
||||
"ui.sessionReview.image.loading": "Laster...",
|
||||
"ui.sessionReview.image.placeholder": "Bilde",
|
||||
"ui.sessionReview.largeDiff.title": "Diff er for stor til å gjengi",
|
||||
"ui.sessionReview.largeDiff.meta": "Grense: {{lines}} linjer / {{limit}}. Nåværende: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Gjengi likevel",
|
||||
|
||||
"ui.lineComment.label.prefix": "Kommenter på ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -9,6 +9,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Dodano",
|
||||
"ui.sessionReview.change.removed": "Usunięto",
|
||||
"ui.sessionReview.change.modified": "Zmodyfikowano",
|
||||
"ui.sessionReview.image.loading": "Ładowanie...",
|
||||
"ui.sessionReview.image.placeholder": "Obraz",
|
||||
"ui.sessionReview.largeDiff.title": "Diff jest zbyt duży, aby go wyrenderować",
|
||||
"ui.sessionReview.largeDiff.meta": "Limit: {{lines}} linii / {{limit}}. Obecnie: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Renderuj mimo to",
|
||||
"ui.lineComment.label.prefix": "Komentarz do ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Komentowanie: ",
|
||||
|
||||
@@ -9,6 +9,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "Добавлено",
|
||||
"ui.sessionReview.change.removed": "Удалено",
|
||||
"ui.sessionReview.change.modified": "Изменено",
|
||||
"ui.sessionReview.image.loading": "Загрузка...",
|
||||
"ui.sessionReview.image.placeholder": "Изображение",
|
||||
"ui.sessionReview.largeDiff.title": "Diff слишком большой для отображения",
|
||||
"ui.sessionReview.largeDiff.meta": "Лимит: {{lines}} строк / {{limit}}. Текущий: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "Отобразить всё равно",
|
||||
"ui.lineComment.label.prefix": "Комментарий к ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Комментирование: ",
|
||||
|
||||
@@ -8,6 +8,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "เพิ่ม",
|
||||
"ui.sessionReview.change.removed": "ลบ",
|
||||
"ui.sessionReview.change.modified": "แก้ไข",
|
||||
"ui.sessionReview.image.loading": "กำลังโหลด...",
|
||||
"ui.sessionReview.image.placeholder": "รูปภาพ",
|
||||
"ui.sessionReview.largeDiff.title": "Diff มีขนาดใหญ่เกินไปจนไม่สามารถแสดงผลได้",
|
||||
"ui.sessionReview.largeDiff.meta": "ขีดจำกัด: {{lines}} บรรทัด / {{limit}}. ปัจจุบัน: {{current}}.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "แสดงผลต่อไป",
|
||||
|
||||
"ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -12,6 +12,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "已添加",
|
||||
"ui.sessionReview.change.removed": "已移除",
|
||||
"ui.sessionReview.change.modified": "已修改",
|
||||
"ui.sessionReview.image.loading": "加载中...",
|
||||
"ui.sessionReview.image.placeholder": "图片",
|
||||
"ui.sessionReview.largeDiff.title": "差异过大,无法渲染",
|
||||
"ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。当前:{{current}}。",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "仍然渲染",
|
||||
|
||||
"ui.lineComment.label.prefix": "评论 ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -12,6 +12,11 @@ export const dict = {
|
||||
"ui.sessionReview.change.added": "已新增",
|
||||
"ui.sessionReview.change.removed": "已移除",
|
||||
"ui.sessionReview.change.modified": "已修改",
|
||||
"ui.sessionReview.image.loading": "載入中...",
|
||||
"ui.sessionReview.image.placeholder": "圖片",
|
||||
"ui.sessionReview.largeDiff.title": "差異過大,無法渲染",
|
||||
"ui.sessionReview.largeDiff.meta": "限制:{{lines}} 行 / {{limit}}。目前:{{current}}。",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "仍然渲染",
|
||||
|
||||
"ui.lineComment.label.prefix": "評論 ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -28,3 +28,24 @@ export function checksum(content: string): string | undefined {
|
||||
}
|
||||
return (hash >>> 0).toString(36)
|
||||
}
|
||||
|
||||
export function sampledChecksum(content: string, limit = 500_000): string | undefined {
|
||||
if (!content) return undefined
|
||||
if (content.length <= limit) return checksum(content)
|
||||
|
||||
const size = 4096
|
||||
const points = [
|
||||
0,
|
||||
Math.floor(content.length * 0.25),
|
||||
Math.floor(content.length * 0.5),
|
||||
Math.floor(content.length * 0.75),
|
||||
content.length - size,
|
||||
]
|
||||
const hashes = points
|
||||
.map((point) => {
|
||||
const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2)))
|
||||
return checksum(content.slice(start, start + size)) ?? ""
|
||||
})
|
||||
.join(":")
|
||||
return `${content.length}:${hashes}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user