mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-29 00:10:47 +08:00
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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
5acfffb352a47e052673f7ecc0c87653bc6c8ec4
|
||||
2011f2b06e7207498aa084a25583c3c96c782c2d
|
||||
|
||||
Reference in New Issue
Block a user