Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add syntax highlighting to patch and commit changesets
Sebastian Martinez committed 2 years ago
commit 288f55da7667d0678d4c1abc248da2cff54891e4
parent 8eaf39e6aef1881ce29f7fc5a3216097b7890be1
10 files changed +218 -84
modified httpd-client/index.ts
@@ -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,
modified httpd-client/lib/project.ts
@@ -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 {
modified httpd-client/lib/project/commit.ts
@@ -1,17 +1,34 @@
import type { z } from "zod";
export type {
-
  Commits,
-
  HunkLine,
-
  Hunks,
  Commit,
-
  Diff,
  CommitHeader,
-
  DiffFile,
+
  Commits,
+
  Diff,
  DiffContent,
+
  DiffFile,
+
  HunkLine,
+
  Hunks,
};

-
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(),
added src/components/Observer.svelte
@@ -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} />
modified src/views/projects/Changeset.svelte
@@ -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>
modified src/views/projects/Changeset/FileDiff.svelte
@@ -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}
modified src/views/projects/Commit.svelte
@@ -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>
modified src/views/projects/Patch.svelte
@@ -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>
modified src/views/projects/router.ts
@@ -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;
    }
  }
modified tests/support/heartwood-version
@@ -1 +1 @@
-
5acfffb352a47e052673f7ecc0c87653bc6c8ec4
+
2011f2b06e7207498aa084a25583c3c96c782c2d