mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-30 16:50:47 +08:00
316 lines
8.7 KiB
Svelte
316 lines
8.7 KiB
Svelte
<script lang="ts">
|
|
import type { DiffContent } from "@bindings/diff/DiffContent";
|
|
import type { FileDiff } from "@bindings/diff/FileDiff";
|
|
import type { Modification } from "@bindings/diff/Modification";
|
|
|
|
import File from "@app/components/File.svelte";
|
|
import escape from "lodash/escape";
|
|
|
|
interface Props {
|
|
filePath: string;
|
|
oldFilePath?: string | undefined;
|
|
fileDiff: DiffContent;
|
|
headerBadgeCaption: FileDiff["status"];
|
|
expanded: boolean;
|
|
}
|
|
|
|
const {
|
|
filePath,
|
|
oldFilePath = undefined,
|
|
fileDiff,
|
|
headerBadgeCaption,
|
|
expanded,
|
|
}: Props = $props();
|
|
|
|
function lineNumberR(line: Modification): string | number {
|
|
switch (line.type) {
|
|
case "addition": {
|
|
return line.lineNo;
|
|
}
|
|
case "context": {
|
|
return line.lineNoNew;
|
|
}
|
|
case "deletion": {
|
|
return " ";
|
|
}
|
|
}
|
|
}
|
|
|
|
function lineNumberL(line: Modification): string | number {
|
|
switch (line.type) {
|
|
case "addition": {
|
|
return " ";
|
|
}
|
|
case "context": {
|
|
return line.lineNoOld;
|
|
}
|
|
case "deletion": {
|
|
return line.lineNo;
|
|
}
|
|
}
|
|
}
|
|
|
|
function lineSign(line: Modification): string {
|
|
switch (line.type) {
|
|
case "addition": {
|
|
return "+";
|
|
}
|
|
case "context": {
|
|
return " ";
|
|
}
|
|
case "deletion": {
|
|
return "-";
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.container {
|
|
font-size: var(--font-size-small);
|
|
overflow-x: auto;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
.browse {
|
|
margin-left: auto;
|
|
}
|
|
.expand-button {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
margin-right: 0.5rem;
|
|
}
|
|
.diff {
|
|
font-family: var(--font-family-monospace);
|
|
table-layout: fixed;
|
|
border-collapse: collapse;
|
|
margin: 0.5rem 0;
|
|
-webkit-touch-callout: initial;
|
|
-webkit-user-select: text;
|
|
user-select: text;
|
|
}
|
|
.diff-line {
|
|
vertical-align: top;
|
|
}
|
|
.diff-line.type-addition > * {
|
|
background-color: var(--color-fill-diff-green-light);
|
|
}
|
|
.diff-line.type-deletion > * {
|
|
background-color: var(--color-fill-diff-red-light);
|
|
}
|
|
|
|
.diff-line.selected > * {
|
|
background-color: var(--color-fill-float-hover);
|
|
}
|
|
.diff-line.selected.type-addition > * {
|
|
background-color: var(--color-fill-diff-green);
|
|
}
|
|
.diff-line.selected.type-deletion > * {
|
|
background-color: var(--color-fill-diff-red);
|
|
}
|
|
|
|
.type-addition > .diff-line-number,
|
|
.type-addition > .diff-line-type {
|
|
color: var(--color-foreground-success);
|
|
}
|
|
.type-deletion > .diff-line-number,
|
|
.type-deletion > .diff-line-type {
|
|
color: var(--color-foreground-red);
|
|
}
|
|
|
|
.diff-line.selected .selection-indicator-left {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
.type-addition.diff-line.selected .selection-indicator-left {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
.type-deletion.diff-line.selected .selection-indicator-left {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
|
|
.diff-line.selected .selection-indicator-right {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
.type-addition.diff-line.selected .selection-indicator-right {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
.type-deletion.diff-line.selected .selection-indicator-right {
|
|
background-color: var(--color-fill-secondary);
|
|
}
|
|
|
|
.selection-start {
|
|
box-shadow: 0 -1px 0 0 var(--color-fill-secondary);
|
|
z-index: 1;
|
|
}
|
|
.selection-end {
|
|
box-shadow: 0 1px 0 0 var(--color-fill-secondary);
|
|
z-index: 1;
|
|
}
|
|
|
|
.selection-start.selection-end {
|
|
box-shadow: 0 0 0 1px var(--color-fill-secondary);
|
|
z-index: 1;
|
|
}
|
|
|
|
.diff-line-number {
|
|
font-family: var(--font-family-monospace);
|
|
text-align: right;
|
|
user-select: none;
|
|
line-height: 1.5rem;
|
|
min-width: 3rem;
|
|
color: var(--color-foreground-disabled);
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
.diff-line-number.left {
|
|
position: relative;
|
|
padding: 0 0.5rem 0 0.75rem;
|
|
}
|
|
.selection-indicator-left {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
}
|
|
.selection-indicator-right {
|
|
display: none; /* FIXME: fix the selection indicator */
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
}
|
|
.diff-line-number.right {
|
|
padding: 0 0.75rem 0 0.5rem;
|
|
}
|
|
.diff-line-content {
|
|
color: unset !important;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
width: 100%;
|
|
padding-right: 0.5rem;
|
|
}
|
|
.diff-line-type {
|
|
text-align: center;
|
|
padding-left: 0.75rem;
|
|
padding-right: 0.75rem;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
.diff-expand-header {
|
|
padding-left: 0.5rem;
|
|
color: var(--color-foreground-dim);
|
|
}
|
|
.added {
|
|
color: var(--color-foreground-success);
|
|
background-color: var(--color-fill-diff-green-light);
|
|
}
|
|
.deleted {
|
|
color: var(--color-foreground-red);
|
|
background-color: var(--color-fill-diff-red-light);
|
|
}
|
|
.moved,
|
|
.copied {
|
|
color: var(--color-foreground-dim);
|
|
background: var(--color-fill-ghost);
|
|
}
|
|
</style>
|
|
|
|
{#snippet styledPath(fullPath: string)}
|
|
<!-- prettier-ignore -->
|
|
<span class="txt-small" style:white-space="nowrap" style:-webkit-touch-callout="initial" style:-webkit-user-select="text" style:user-select="text"><span style:color="var(--color-fill-gray)" style:font-weight="var(--font-weight-regular)">{fullPath.match(/^.*\/|/)?.values().next().value}</span><span style:font-weight="var(--font-weight-semibold)">{fullPath.split("/").slice(-1)}</span></span>
|
|
{/snippet}
|
|
|
|
<File {expanded}>
|
|
{#snippet leftHeader()}
|
|
{#if (headerBadgeCaption === "moved" || headerBadgeCaption === "copied") && oldFilePath}
|
|
<span style="display: flex; align-items: center; flex-wrap: wrap;">
|
|
{@render styledPath(filePath)}
|
|
<span style:padding="0 0.5rem">→</span>
|
|
{@render styledPath(filePath)}
|
|
</span>
|
|
{:else}
|
|
{@render styledPath(filePath)}
|
|
{/if}
|
|
|
|
{#if headerBadgeCaption === "added"}
|
|
<span class="global-counter added">added</span>
|
|
{:else if headerBadgeCaption === "deleted"}
|
|
<span class="global-counter deleted">deleted</span>
|
|
{:else if headerBadgeCaption === "moved"}
|
|
<span class="global-counter moved">moved</span>
|
|
{:else if headerBadgeCaption === "copied"}
|
|
<span class="global-counter copied">copied</span>
|
|
{/if}
|
|
{/snippet}
|
|
|
|
{#snippet children()}
|
|
<div class="container">
|
|
{#if fileDiff.type === "plain"}
|
|
{#if fileDiff.hunks.length > 0}
|
|
<table class="diff" data-file-diff-select>
|
|
{#each fileDiff.hunks as hunk, hunkIdx}
|
|
<!-- svelte-ignore node_invalid_placement_ssr -->
|
|
<tr class="diff-line hunk-header">
|
|
<td colspan={2} style:position="relative">
|
|
<div class="selection-indicator-left"></div>
|
|
</td>
|
|
<td
|
|
colspan={6}
|
|
class="diff-expand-header"
|
|
style:position="relative">
|
|
{hunk.header}
|
|
<div class="selection-indicator-right"></div>
|
|
</td>
|
|
</tr>
|
|
{#each hunk.lines as line, lineIdx}
|
|
<!-- svelte-ignore node_invalid_placement_ssr -->
|
|
<tr
|
|
style="position: relative;"
|
|
class={`diff-line type-${line.type}`}>
|
|
<td
|
|
id={[filePath, "H" + hunkIdx, "L" + lineIdx].join("-")}
|
|
class="diff-line-number left">
|
|
<div class="selection-indicator-left"></div>
|
|
{lineNumberL(line)}
|
|
</td>
|
|
<td class="diff-line-number right">
|
|
{lineNumberR(line)}
|
|
</td>
|
|
<td class="diff-line-type" data-line-type={line.type}>
|
|
{lineSign(line)}
|
|
</td>
|
|
<td class="diff-line-content">
|
|
{#if line.highlight?.items.length === 0 || line.line === ""}
|
|
<br />
|
|
{:else if line.highlight}
|
|
{@html line.highlight.items
|
|
.map(
|
|
s =>
|
|
`<span class="syntax ${s.style}">${escape(s.item)}</span>`,
|
|
)
|
|
.join("")}
|
|
{:else}
|
|
{line.line}
|
|
{/if}
|
|
</td>
|
|
<td class="selection-indicator-right"></td>
|
|
</tr>
|
|
{/each}
|
|
{/each}
|
|
</table>
|
|
{:else}
|
|
<div style:margin="1rem 0">Empty file</div>
|
|
{/if}
|
|
{:else}
|
|
<div style:margin="1rem 0">Empty file</div>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
</File>
|