Add syntax highlighting to patch and commit changesets

- This commit doesn't change the way syntax highlighting is done.
- But due to changes in `radicle-httpd` we can highlight the entire
  files of a changeset, and then replace the plain text lines with
  highlighted ones.
- Also highlights only the files currently visible of a changeset.

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez
2023-10-05 13:55:00 +02:00
parent 8eaf39e6ae
commit 288f55da76
10 changed files with 218 additions and 84 deletions
+3 -1
View File
@@ -1,7 +1,6 @@
import type { BaseUrl } from "./lib/fetcher.js";
import type {
Blob,
CommitBlob,
Project,
Remote,
Tree,
@@ -10,8 +9,10 @@ import type {
import type { Comment } from "./lib/project/comment.js";
import type {
Commit,
CommitBlob,
CommitHeader,
Diff,
DiffBlob,
DiffContent,
DiffFile,
HunkLine,
@@ -46,6 +47,7 @@ export type {
CommitBlob,
CommitHeader,
Diff,
DiffBlob,
DiffContent,
DiffFile,
DiffResponse,
+2 -9
View File
@@ -29,6 +29,7 @@ import {
} from "zod";
import {
diffBlobSchema,
commitHeaderSchema,
commitSchema,
commitsSchema,
@@ -86,14 +87,6 @@ const blobSchema = object({
export type Blob = z.infer<typeof blobSchema>;
const commitBlobSchema = object({
binary: boolean(),
content: optional(string()),
lastCommit: commitHeaderSchema,
});
export type CommitBlob = z.infer<typeof commitBlobSchema>;
const treeEntrySchema = object({
path: string(),
name: string(),
@@ -138,7 +131,7 @@ export type DiffResponse = z.infer<typeof diffResponseSchema>;
const diffResponseSchema = object({
commits: array(commitHeaderSchema),
diff: diffSchema,
files: record(string(), commitBlobSchema),
files: record(string(), diffBlobSchema),
});
export class Client {
+42 -8
View File
@@ -1,17 +1,34 @@
import type { z } from "zod";
export type {
Commit,
CommitHeader,
Commits,
Diff,
DiffContent,
DiffFile,
HunkLine,
Hunks,
Commit,
Diff,
CommitHeader,
DiffFile,
DiffContent,
};
import { array, literal, number, object, optional, string, union } from "zod";
export { commitHeaderSchema, diffSchema, commitSchema, commitsSchema };
import {
array,
boolean,
literal,
number,
object,
optional,
record,
string,
union,
} from "zod";
export {
commitBlobSchema,
commitHeaderSchema,
commitSchema,
commitsSchema,
diffBlobSchema,
diffSchema,
};
const gitPersonSchema = object({
name: string(),
@@ -29,6 +46,22 @@ const commitHeaderSchema = object({
committer: gitPersonSchema.merge(object({ time: number() })),
});
const diffBlobSchema = object({
binary: boolean(),
content: string(),
id: string(),
lastCommit: commitHeaderSchema,
});
export type DiffBlob = z.infer<typeof diffBlobSchema>;
const commitBlobSchema = object({
binary: boolean(),
content: string(),
});
export type CommitBlob = z.infer<typeof commitBlobSchema>;
type AdditionHunkLine = z.infer<typeof additionHunkLineSchema>;
const additionHunkLineSchema = object({
@@ -146,12 +179,13 @@ const commitSchema = object({
commit: commitHeaderSchema,
diff: diffSchema,
branches: array(string()),
files: record(string(), commitBlobSchema),
});
type Commits = z.infer<typeof commitsSchema>;
const commitsSchema = object({
commits: array(commitSchema),
commits: array(commitSchema.omit({ files: true })),
stats: object({
commits: number(),
branches: number(),
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts" context="module">
export function intersection(
node: HTMLElement,
observer: IntersectionObserver | undefined,
) {
if (!observer) return;
observer.observe(node);
return {
destroy() {
observer.unobserve(node);
},
};
}
</script>
<script lang="ts">
import { onDestroy } from "svelte";
let observer: IntersectionObserver | undefined = undefined;
let filesVisibility = new Set<string>();
if ("IntersectionObserver" in window) {
observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
filesVisibility = filesVisibility.add(
entry.target.id.replace("observer:", ""),
);
}
});
});
}
onDestroy(() => {
if (observer) {
observer.disconnect();
}
});
</script>
<slot {observer} {filesVisibility} />
+72 -53
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import type {
BaseUrl,
Diff,
CommitBlob,
Diff,
DiffContent,
DiffFile,
} from "@httpd-client";
@@ -11,14 +11,13 @@
import FileDiff from "@app/views/projects/Changeset/FileDiff.svelte";
import FileLocationChange from "@app/views/projects/Changeset/FileLocationChange.svelte";
import Observer, { intersection } from "@app/components/Observer.svelte";
export let diff: Diff;
// This only is needed in commit view where we have a useful revision.
export let revision: string | undefined = undefined;
// This only is needed for patch changesets where we have different files with different last commits.
export let files: Record<string, CommitBlob> = {};
export let files: Record<string, CommitBlob>;
export let baseUrl: BaseUrl;
export let projectId: string;
export let revision: string;
const diffDescription = ({
modified,
@@ -86,57 +85,77 @@
</span>
</div>
<div class="diff-listing">
{#each diff.added as file}
<FileDiff
{projectId}
{baseUrl}
revision={revision ?? files[file.new.oid].lastCommit.id}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.new) }}
headerBadgeCaption="added" />
{/each}
{#each diff.deleted as file}
<FileDiff
{projectId}
{baseUrl}
revision={revision ?? files[file.old.oid].lastCommit.id}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.old) }}
headerBadgeCaption="deleted" />
{/each}
{#each diff.modified as file}
<FileDiff
{projectId}
{baseUrl}
revision={revision ?? files[file.new.oid].lastCommit.id}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.new) }} />
{/each}
{#each diff.moved as file}
{#if file.diff}
<FileDiff
{projectId}
{baseUrl}
{revision}
filePath={file.newPath}
oldFilePath={file.oldPath}
fileDiff={file.diff}
headerBadgeCaption="moved" />
{:else}
<Observer let:filesVisibility let:observer>
{#each diff.added as file}
<div use:intersection={observer} id={"observer:" + file.path}>
<FileDiff
{projectId}
{baseUrl}
{revision}
visible={filesVisibility.has(file.path)}
content={files[file.new.oid]?.content}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.new) }}
headerBadgeCaption="added" />
</div>
{/each}
{#each diff.deleted as file}
<div use:intersection={observer} id={"observer:" + file.path}>
<FileDiff
{projectId}
{baseUrl}
{revision}
visible={filesVisibility.has(file.path)}
oldContent={files[file.old.oid]?.content}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.old) }}
headerBadgeCaption="deleted" />
</div>
{/each}
{#each diff.modified as file}
<div use:intersection={observer} id={"observer:" + file.path}>
<FileDiff
{projectId}
{baseUrl}
{revision}
visible={filesVisibility.has(file.path)}
oldContent={files[file.old.oid]?.content}
content={files[file.new.oid]?.content}
filePath={file.path}
fileDiff={{ ...file.diff, type: getFileType(file.diff, file.new) }} />
</div>
{/each}
{#each diff.moved as file}
{#if file.diff}
<div use:intersection={observer} id={"observer:" + file.newPath}>
<FileDiff
{projectId}
{baseUrl}
{revision}
content=""
visible={filesVisibility.has(file.newPath)}
filePath={file.newPath}
oldFilePath={file.oldPath}
fileDiff={file.diff}
headerBadgeCaption="moved" />
</div>
{:else}
<FileLocationChange
{projectId}
{baseUrl}
{revision}
newPath={file.newPath}
oldPath={file.oldPath}
mode="moved" />
{/if}
{/each}
{#each diff.copied as file}
<FileLocationChange
{projectId}
{baseUrl}
newPath={file.newPath}
oldPath={file.oldPath}
mode="moved" />
{/if}
{/each}
{#each diff.copied as file}
<FileLocationChange
{projectId}
{baseUrl}
newPath={file.newPath}
oldPath={file.oldPath}
mode="copied" />
{/each}
mode="copied" />
{/each}
</Observer>
</div>
+44 -2
View File
@@ -1,12 +1,17 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import type { BaseUrl, DiffContent, HunkLine } from "@httpd-client";
import { onDestroy, onMount } from "svelte";
import { toHtml } from "hast-util-to-html";
import * as Syntax from "@app/lib/syntax";
import Badge from "@app/components/Badge.svelte";
import Icon from "@app/components/Icon.svelte";
import Link from "@app/components/Link.svelte";
export let filePath: string;
export let oldContent: string | undefined = undefined;
export let content: string | undefined = undefined;
export let oldFilePath: string | undefined = undefined;
export let fileDiff: DiffContent;
export let revision: string | undefined = undefined;
@@ -18,9 +23,11 @@
| undefined = undefined;
export let baseUrl: BaseUrl;
export let projectId: string;
export let visible: boolean = false;
let collapsed = false;
let selection: Selection | undefined = undefined;
let highlighting: { new?: string[]; old?: string[] } | undefined = undefined;
onMount(() => {
window.addEventListener("click", deselectHandler);
@@ -39,6 +46,10 @@
}
});
$: if (visible) {
void highlightContent().then(output => (highlighting = output));
}
onDestroy(() => {
window.removeEventListener("click", deselectHandler);
window.removeEventListener("hashchange", updateSelection);
@@ -55,6 +66,24 @@
}
}
async function highlightContent() {
const extension = filePath.split(".").pop();
const highlighted: { new?: string[]; old?: string[] } = {};
if (extension) {
if (content) {
highlighted["new"] = toHtml(
await Syntax.highlight(content, extension),
).split("\n");
}
if (oldContent) {
highlighted["old"] = toHtml(
await Syntax.highlight(oldContent, extension),
).split("\n");
}
}
return Object.entries(highlighted).length > 0 ? highlighted : undefined;
}
function updateSelection() {
const fragment = window.location.hash.substring(1);
const match = fragment.match(/(.+):H(\d+)L(\d+)(H(\d+)L(\d+))?/);
@@ -298,6 +327,7 @@
padding: 0 0.75rem 0 0.5rem;
}
.diff-line-content {
color: unset !important;
white-space: pre-wrap;
overflow-wrap: anywhere;
width: 100%;
@@ -395,7 +425,19 @@
<td class="diff-line-type" data-line-type={line.type}>
{lineSign(line)}
</td>
<td class="diff-line-content">{line.line}</td>
<td class="diff-line-content">
{#if highlighting}
{#if line.type === "addition" && highlighting.new}
{@html highlighting.new[line.lineNo - 1]}
{:else if line.type === "context" && highlighting.new}
{@html highlighting.new[line.lineNoNew - 1]}
{:else if line.type === "deletion" && highlighting.old}
{@html highlighting.old[line.lineNo - 1]}
{/if}
{:else}
{line.line}
{/if}
</td>
</tr>
{/each}
{/each}
+3 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Commit, BaseUrl, Project } from "@httpd-client";
import type { BaseUrl, Commit, Project } from "@httpd-client";
import { formatCommit } from "@app/lib/utils";
@@ -70,8 +70,9 @@
<CommitAuthorship {header} />
</div>
<Changeset
projectId={project.id}
{baseUrl}
projectId={project.id}
files={commit.files}
diff={commit.diff}
revision={commit.commit.id} />
</div>
+4 -2
View File
@@ -547,9 +547,10 @@
{#if view.name === "diff"}
<div style:margin-top="1rem">
<Changeset
projectId={project.id}
{baseUrl}
projectId={project.id}
revision={view.toCommit}
files={view.files}
diff={view.diff} />
</div>
{:else if view.name === "activity"}
@@ -585,8 +586,9 @@
{:else if view.name === "files"}
<div style:margin-top="1rem">
<Changeset
projectId={project.id}
{baseUrl}
projectId={project.id}
revision={view.oid}
files={view.files}
diff={view.diff} />
</div>
+6 -6
View File
@@ -9,6 +9,7 @@ import type {
CommitBlob,
CommitHeader,
Diff,
DiffBlob,
Issue,
IssueState,
Patch,
@@ -133,7 +134,6 @@ export type ProjectLoadedRoute =
params: {
baseUrl: BaseUrl;
project: Project;
commit: Commit;
};
}
@@ -151,7 +151,6 @@ export type ProjectLoadedRoute =
params: {
baseUrl: BaseUrl;
project: Project;
issues: Issue[];
state: IssueState["status"];
};
@@ -168,7 +167,6 @@ export type ProjectLoadedRoute =
params: {
baseUrl: BaseUrl;
project: Project;
patches: Patch[];
state: PatchState["status"];
};
@@ -178,7 +176,6 @@ export type ProjectLoadedRoute =
params: {
baseUrl: BaseUrl;
project: Project;
patch: Patch;
view: PatchView;
};
@@ -196,6 +193,7 @@ export type PatchView =
| {
name: "commits" | "files";
revision: string;
oid: string;
diff: Diff;
commits: CommitHeader[];
files: Record<string, CommitBlob>;
@@ -203,6 +201,7 @@ export type PatchView =
| {
name: "diff";
diff: Diff;
files: Record<string, DiffBlob>;
fromCommit: string;
toCommit: string;
};
@@ -547,6 +546,7 @@ async function loadPatchView(
view = {
name: route.view?.name,
revision: revision.id,
oid: revision.oid,
diff,
commits,
files,
@@ -555,13 +555,13 @@ async function loadPatchView(
}
case "diff": {
const { fromCommit, toCommit } = route.view;
const { diff } = await api.project.getDiff(
const { diff, files } = await api.project.getDiff(
route.project,
fromCommit,
toCommit,
);
view = { name: "diff", fromCommit, toCommit, diff };
view = { name: "diff", fromCommit, toCommit, files, diff };
break;
}
}
+1 -1
View File
@@ -1 +1 @@
5acfffb352a47e052673f7ecc0c87653bc6c8ec4
2011f2b06e7207498aa084a25583c3c96c782c2d