mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-29 08:10:59 +08:00
Add unified-diff copy buttons to the commit diff
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { writeToClipboard } from "@app/lib/invoke";
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
interface Props {
|
||||
// Produces the text lazily, so large content isn't built until clicked.
|
||||
text: () => string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { text, title = "Copy" }: Props = $props();
|
||||
|
||||
let copied = $state(false);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function copy() {
|
||||
await writeToClipboard(text());
|
||||
copied = true;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
copied = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$effect(() => () => clearTimeout(timer));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.copy {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.copy:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<button class="copy" {title} onclick={copy}>
|
||||
<Icon name={copied ? "checkmark" : "clipboard"} />
|
||||
</button>
|
||||
@@ -4,7 +4,9 @@
|
||||
import escape from "lodash/escape";
|
||||
|
||||
import type { DiffRow } from "@app/lib/diffRows";
|
||||
import { fileDiffToText } from "@app/lib/diffText";
|
||||
|
||||
import CopyButton from "@app/components/CopyButton.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Path from "@app/components/Path.svelte";
|
||||
|
||||
@@ -191,6 +193,7 @@
|
||||
<span class="del">-{row.file.diff.stats.deletions}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<CopyButton text={() => fileDiffToText(row.file)} title="Copy file diff" />
|
||||
</div>
|
||||
{:else if row.type === "hunk-header"}
|
||||
<div class="line hunk-header">
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Diff } from "@bindings/diff/Diff";
|
||||
import type { FileDiff } from "@bindings/diff/FileDiff";
|
||||
|
||||
// Selection across off-screen lines isn't possible once the diff is
|
||||
// virtualized, so these build copyable unified-diff text straight from the
|
||||
// model for the "copy file" / "copy diff" buttons.
|
||||
|
||||
function filePath(file: FileDiff): string {
|
||||
if (file.status === "moved" || file.status === "copied") {
|
||||
return `${file.oldPath} → ${file.newPath}`;
|
||||
}
|
||||
return file.path;
|
||||
}
|
||||
|
||||
// Diff line/header text from the backend keeps its trailing newline; strip it
|
||||
// so join("\n") doesn't double every line.
|
||||
function stripEol(s: string): string {
|
||||
return s.replace(/\r?\n$/, "");
|
||||
}
|
||||
|
||||
export function fileDiffToText(file: FileDiff): string {
|
||||
const lines: string[] = [filePath(file)];
|
||||
if (file.diff.type === "plain") {
|
||||
for (const hunk of file.diff.hunks) {
|
||||
lines.push(stripEol(hunk.header));
|
||||
for (const m of hunk.lines) {
|
||||
const prefix =
|
||||
m.type === "addition" ? "+" : m.type === "deletion" ? "-" : " ";
|
||||
lines.push(`${prefix}${stripEol(m.line)}`);
|
||||
}
|
||||
}
|
||||
} else if (file.diff.type === "binary") {
|
||||
lines.push("Binary file");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function diffToText(diff: Diff): string {
|
||||
return diff.files.map(fileDiffToText).join("\n\n");
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import { diffToText } from "@app/lib/diffText";
|
||||
import * as router from "@app/lib/router";
|
||||
import {
|
||||
absoluteTimestamp,
|
||||
@@ -14,6 +15,7 @@
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import Changeset from "@app/components/Changeset.svelte";
|
||||
import CopyButton from "@app/components/CopyButton.svelte";
|
||||
import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
|
||||
import ExternalLink from "@app/components/ExternalLink.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
@@ -227,6 +229,7 @@
|
||||
{pluralize("file", diff.stats.filesChanged)} changed
|
||||
</div>
|
||||
<DiffStatBadge stats={diff.stats} />
|
||||
<CopyButton text={() => diffToText(diff)} title="Copy diff" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user