|
|
@@ -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 : "",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </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)}
|
|
|
/>
|
|
|
- </Show>
|
|
|
- )}
|
|
|
+ )}
|
|
|
+ </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>
|