mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Trim the diff payload to a manifest
The tree-sitter highlighter goes with it: the hunk types name its `Paint` spans and it implements `ToPretty` for them, so neither side can be removed on its own.
This commit is contained in:
@@ -27,24 +27,6 @@ tauri-plugin-clipboard-manager = { version = "2.3.2" }
|
||||
tauri-plugin-fs = { version = "2.5.0" }
|
||||
tempfile = { version = "3.19.0" }
|
||||
thiserror = { version = "2.0.12" }
|
||||
tree-sitter-bash = { version = "0.23.3" }
|
||||
tree-sitter-c = { version = "0.23.2" }
|
||||
tree-sitter-css = { version = "0.23.1" }
|
||||
tree-sitter-diff = { version = "0.1.0" }
|
||||
tree-sitter-go = { version = "0.23.4" }
|
||||
tree-sitter-highlight = { version = "0.25.3" }
|
||||
tree-sitter-html = { version = "0.23.2" }
|
||||
tree-sitter-javascript = { version = "0.23.1" }
|
||||
tree-sitter-jsdoc = { version = "0.23.2" }
|
||||
tree-sitter-json = { version = "0.24.8" }
|
||||
tree-sitter-md = { version = "0.3.2" }
|
||||
tree-sitter-python = { version = "0.23.4" }
|
||||
tree-sitter-regex = { version = "0.24.3" }
|
||||
tree-sitter-ruby = { version = "0.23.1" }
|
||||
tree-sitter-rust = { version = "0.23.2" }
|
||||
tree-sitter-svelte-ng = { version = "1.0.2" }
|
||||
tree-sitter-toml-ng = { version = "0.7.0" }
|
||||
tree-sitter-typescript = { version = "0.23.2" }
|
||||
ts-rs = { version = "12.0.1", features = [
|
||||
"serde-json-impl",
|
||||
"no-serde-warnings",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DiffContent } from "./DiffContent";
|
||||
import type { DiffFile } from "./DiffFile";
|
||||
|
||||
export type Added = { path: string; diff: DiffContent; new: DiffFile };
|
||||
export type Added = { path: string; diff: DiffContent };
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Line } from "../syntax/Line";
|
||||
|
||||
export type Addition = { line: string; lineNo: number; highlight: Line | null };
|
||||
@@ -1,11 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DiffContent } from "./DiffContent";
|
||||
import type { DiffFile } from "./DiffFile";
|
||||
|
||||
export type Copied = {
|
||||
oldPath: string;
|
||||
newPath: string;
|
||||
old: DiffFile;
|
||||
new: DiffFile;
|
||||
diff: DiffContent;
|
||||
};
|
||||
export type Copied = { oldPath: string; newPath: string; diff: DiffContent };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DiffContent } from "./DiffContent";
|
||||
import type { DiffFile } from "./DiffFile";
|
||||
|
||||
export type Deleted = { path: string; diff: DiffContent; old: DiffFile };
|
||||
export type Deleted = { path: string; diff: DiffContent };
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Line } from "../syntax/Line";
|
||||
|
||||
export type Deletion = { line: string; lineNo: number; highlight: Line | null };
|
||||
@@ -1,11 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { EofNewLine } from "./EofNewLine";
|
||||
import type { FileStats } from "./FileStats";
|
||||
import type { Hunks } from "./Hunks";
|
||||
|
||||
/**
|
||||
* What kind of change a file carries. `Plain` keeps its stats; the other two
|
||||
* exist so the app can label a file it cannot render lines for.
|
||||
*/
|
||||
export type DiffContent = { "type": "binary" } | {
|
||||
"type": "plain";
|
||||
hunks: Hunks;
|
||||
stats: FileStats;
|
||||
eof: EofNewLine;
|
||||
} | { "type": "empty" };
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FileMode } from "./FileMode";
|
||||
|
||||
export type DiffFile = { oid: string; mode: FileMode };
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type EofNewLine =
|
||||
| "oldMissing"
|
||||
| "newMissing"
|
||||
| "bothMissing"
|
||||
| "noneMissing";
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FileMode = "blob" | "blobExecutable" | "tree" | "link" | "commit";
|
||||
@@ -1,9 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Modification } from "./Modification";
|
||||
|
||||
export type Hunk = {
|
||||
header: string;
|
||||
lines: Array<Modification>;
|
||||
old: { start: number; end: number };
|
||||
new: { start: number; end: number };
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Hunk } from "./Hunk";
|
||||
|
||||
export type Hunks = Array<Hunk>;
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Line = Array<number>;
|
||||
@@ -1,15 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Line } from "../syntax/Line";
|
||||
import type { Addition } from "./Addition";
|
||||
import type { Deletion } from "./Deletion";
|
||||
|
||||
export type Modification =
|
||||
| { "type": "addition" } & Addition
|
||||
| { "type": "deletion" } & Deletion
|
||||
| {
|
||||
"type": "context";
|
||||
line: string;
|
||||
lineNoOld: number;
|
||||
lineNoNew: number;
|
||||
highlight: Line | null;
|
||||
};
|
||||
@@ -1,10 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DiffContent } from "./DiffContent";
|
||||
import type { DiffFile } from "./DiffFile";
|
||||
|
||||
export type Modified = {
|
||||
path: string;
|
||||
diff: DiffContent;
|
||||
old: DiffFile;
|
||||
new: DiffFile;
|
||||
};
|
||||
export type Modified = { path: string; diff: DiffContent };
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DiffContent } from "./DiffContent";
|
||||
import type { DiffFile } from "./DiffFile";
|
||||
|
||||
export type Moved = {
|
||||
oldPath: string;
|
||||
old: DiffFile;
|
||||
newPath: string;
|
||||
new: DiffFile;
|
||||
diff: DiffContent;
|
||||
};
|
||||
export type Moved = { oldPath: string; newPath: string; diff: DiffContent };
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Paint } from "./Paint";
|
||||
|
||||
/**
|
||||
* A styled string that does not contain any `'\n'`.
|
||||
*/
|
||||
export type Label = Paint;
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Label } from "./Label";
|
||||
|
||||
/**
|
||||
* A line of text that has styling and can be displayed.
|
||||
*/
|
||||
export type Line = { items: Array<Label> };
|
||||
@@ -1,6 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* A structure encapsulating an item and styling.
|
||||
*/
|
||||
export type Paint = { item: string; style: string | null };
|
||||
@@ -1,11 +1,20 @@
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
//! A manifest of what a diff touches: which files, how each changed, and how
|
||||
//! many lines either way.
|
||||
//!
|
||||
//! Deliberately not the lines themselves. The app renders diffs from `git
|
||||
//! diff`-format patch text (`get_diff_text`) with `@pierre/diffs`, which parses
|
||||
//! and highlights it client-side, so serialising every hunk here would send the
|
||||
//! same content across the IPC boundary a second time for nobody to read. What
|
||||
//! is left is what the patch text cannot tell the app on its own: whether a file
|
||||
//! is binary or has no textual change at all, and the per-file and overall
|
||||
//! stats.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use radicle_surf as surf;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
use radicle::git;
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
@@ -52,61 +61,50 @@ pub enum FileDiff {
|
||||
impl From<surf::diff::FileDiff> for FileDiff {
|
||||
fn from(value: surf::diff::FileDiff) -> Self {
|
||||
match value {
|
||||
surf::diff::FileDiff::Added(surf::diff::Added { path, diff, new }) => {
|
||||
surf::diff::FileDiff::Added(surf::diff::Added { path, diff, .. }) => {
|
||||
Self::Added(Added {
|
||||
path,
|
||||
diff: diff.into(),
|
||||
new: new.into(),
|
||||
})
|
||||
}
|
||||
surf::diff::FileDiff::Deleted(surf::diff::Deleted { path, diff, old }) => {
|
||||
surf::diff::FileDiff::Deleted(surf::diff::Deleted { path, diff, .. }) => {
|
||||
Self::Deleted(Deleted {
|
||||
path,
|
||||
diff: diff.into(),
|
||||
old: old.into(),
|
||||
})
|
||||
}
|
||||
surf::diff::FileDiff::Modified(surf::diff::Modified {
|
||||
path,
|
||||
diff,
|
||||
old,
|
||||
new,
|
||||
}) => Self::Modified(Modified {
|
||||
path,
|
||||
diff: diff.into(),
|
||||
old: old.into(),
|
||||
new: new.into(),
|
||||
}),
|
||||
surf::diff::FileDiff::Modified(surf::diff::Modified { path, diff, .. }) => {
|
||||
Self::Modified(Modified {
|
||||
path,
|
||||
diff: diff.into(),
|
||||
})
|
||||
}
|
||||
surf::diff::FileDiff::Moved(surf::diff::Moved {
|
||||
old_path,
|
||||
old,
|
||||
new_path,
|
||||
new,
|
||||
diff,
|
||||
..
|
||||
}) => Self::Moved(Moved {
|
||||
old_path,
|
||||
old: old.into(),
|
||||
new_path,
|
||||
new: new.into(),
|
||||
diff: diff.into(),
|
||||
}),
|
||||
surf::diff::FileDiff::Copied(surf::diff::Copied {
|
||||
old_path,
|
||||
new_path,
|
||||
old,
|
||||
new,
|
||||
diff,
|
||||
..
|
||||
}) => Self::Copied(Copied {
|
||||
old_path,
|
||||
new_path,
|
||||
old: old.into(),
|
||||
new: new.into(),
|
||||
diff: diff.into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What kind of change a file carries. `Plain` keeps its stats; the other two
|
||||
/// exist so the app can label a file it cannot render lines for.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
@@ -117,21 +115,15 @@ impl From<surf::diff::FileDiff> for FileDiff {
|
||||
#[ts(export_to = "diff/")]
|
||||
pub enum DiffContent {
|
||||
Binary,
|
||||
Plain {
|
||||
hunks: Hunks,
|
||||
stats: FileStats,
|
||||
eof: EofNewLine,
|
||||
},
|
||||
Plain { stats: FileStats },
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl From<surf::diff::DiffContent> for DiffContent {
|
||||
fn from(value: surf::diff::DiffContent) -> Self {
|
||||
match value {
|
||||
surf::diff::DiffContent::Plain { hunks, stats, eof } => Self::Plain {
|
||||
hunks: hunks.into(),
|
||||
surf::diff::DiffContent::Plain { stats, .. } => Self::Plain {
|
||||
stats: stats.into(),
|
||||
eof: eof.into(),
|
||||
},
|
||||
surf::diff::DiffContent::Binary => Self::Binary,
|
||||
surf::diff::DiffContent::Empty => Self::Empty,
|
||||
@@ -139,25 +131,6 @@ impl From<surf::diff::DiffContent> for DiffContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct DiffFile {
|
||||
#[ts(as = "String")]
|
||||
pub oid: git::Oid,
|
||||
pub mode: FileMode,
|
||||
}
|
||||
|
||||
impl From<surf::diff::DiffFile> for DiffFile {
|
||||
fn from(value: surf::diff::DiffFile) -> Self {
|
||||
Self {
|
||||
oid: value.oid,
|
||||
mode: value.mode.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
@@ -165,7 +138,6 @@ impl From<surf::diff::DiffFile> for DiffFile {
|
||||
pub struct Added {
|
||||
pub path: PathBuf,
|
||||
pub diff: DiffContent,
|
||||
pub new: DiffFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
@@ -175,7 +147,6 @@ pub struct Added {
|
||||
pub struct Deleted {
|
||||
pub path: PathBuf,
|
||||
pub diff: DiffContent,
|
||||
pub old: DiffFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
@@ -184,9 +155,7 @@ pub struct Deleted {
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Moved {
|
||||
pub old_path: PathBuf,
|
||||
pub old: DiffFile,
|
||||
pub new_path: PathBuf,
|
||||
pub new: DiffFile,
|
||||
pub diff: DiffContent,
|
||||
}
|
||||
|
||||
@@ -197,8 +166,6 @@ pub struct Moved {
|
||||
pub struct Copied {
|
||||
pub old_path: PathBuf,
|
||||
pub new_path: PathBuf,
|
||||
pub old: DiffFile,
|
||||
pub new: DiffFile,
|
||||
pub diff: DiffContent,
|
||||
}
|
||||
|
||||
@@ -209,8 +176,6 @@ pub struct Copied {
|
||||
pub struct Modified {
|
||||
pub path: PathBuf,
|
||||
pub diff: DiffContent,
|
||||
pub old: DiffFile,
|
||||
pub new: DiffFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, TS)]
|
||||
@@ -233,52 +198,6 @@ impl From<surf::diff::Stats> for Stats {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all_fields = "camelCase", rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub enum FileMode {
|
||||
Blob,
|
||||
BlobExecutable,
|
||||
Tree,
|
||||
Link,
|
||||
Commit,
|
||||
}
|
||||
|
||||
impl From<surf::diff::FileMode> for FileMode {
|
||||
fn from(value: surf::diff::FileMode) -> Self {
|
||||
match value {
|
||||
surf::diff::FileMode::Blob => Self::Blob,
|
||||
surf::diff::FileMode::BlobExecutable => Self::BlobExecutable,
|
||||
surf::diff::FileMode::Tree => Self::Tree,
|
||||
surf::diff::FileMode::Link => Self::Link,
|
||||
surf::diff::FileMode::Commit => Self::Commit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all_fields = "camelCase", rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub enum EofNewLine {
|
||||
OldMissing,
|
||||
NewMissing,
|
||||
BothMissing,
|
||||
NoneMissing,
|
||||
}
|
||||
|
||||
impl From<surf::diff::EofNewLine> for EofNewLine {
|
||||
fn from(value: surf::diff::EofNewLine) -> Self {
|
||||
match value {
|
||||
surf::diff::EofNewLine::OldMissing => Self::OldMissing,
|
||||
surf::diff::EofNewLine::NewMissing => Self::NewMissing,
|
||||
surf::diff::EofNewLine::BothMissing => Self::BothMissing,
|
||||
surf::diff::EofNewLine::NoneMissing => Self::NoneMissing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
@@ -296,130 +215,3 @@ impl From<surf::diff::FileStats> for FileStats {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all_fields = "camelCase",
|
||||
rename_all = "camelCase"
|
||||
)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub enum Modification {
|
||||
Addition(Addition),
|
||||
Deletion(Deletion),
|
||||
Context {
|
||||
line: String,
|
||||
line_no_old: u32,
|
||||
line_no_new: u32,
|
||||
highlight: Option<crate::syntax::Line>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<surf::diff::Modification> for Modification {
|
||||
fn from(value: surf::diff::Modification) -> Self {
|
||||
match value {
|
||||
surf::diff::Modification::Addition(surf::diff::Addition { line, line_no }) => {
|
||||
Modification::Addition(Addition {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no,
|
||||
highlight: None,
|
||||
})
|
||||
}
|
||||
surf::diff::Modification::Deletion(surf::diff::Deletion { line, line_no }) => {
|
||||
Modification::Deletion(Deletion {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no,
|
||||
highlight: None,
|
||||
})
|
||||
}
|
||||
surf::diff::Modification::Context {
|
||||
line,
|
||||
line_no_old,
|
||||
line_no_new,
|
||||
} => Modification::Context {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no_old,
|
||||
line_no_new,
|
||||
highlight: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq, Eq, TS)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Hunks(pub Vec<Hunk>);
|
||||
|
||||
impl From<Vec<Hunk>> for Hunks {
|
||||
fn from(hunks: Vec<Hunk>) -> Self {
|
||||
Self(hunks)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surf::diff::Hunks<surf::diff::Modification>> for Hunks {
|
||||
fn from(value: surf::diff::Hunks<surf::diff::Modification>) -> Self {
|
||||
Self(value.0.into_iter().map(Into::into).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Hunk {
|
||||
pub header: String,
|
||||
pub lines: Vec<Modification>,
|
||||
pub old: Range<u32>,
|
||||
pub new: Range<u32>,
|
||||
}
|
||||
|
||||
impl From<surf::diff::Hunk<surf::diff::Modification>> for Hunk {
|
||||
fn from(value: surf::diff::Hunk<surf::diff::Modification>) -> Self {
|
||||
Self {
|
||||
header: String::from_utf8_lossy(value.header.as_bytes()).to_string(),
|
||||
lines: value.lines.into_iter().map(Into::into).collect::<Vec<_>>(),
|
||||
old: value.old,
|
||||
new: value.new,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Line(pub(crate) Vec<u8>);
|
||||
|
||||
impl Line {
|
||||
/// Create a new line.
|
||||
pub fn new(item: Vec<u8>) -> Self {
|
||||
Self(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surf::diff::Line> for Line {
|
||||
fn from(value: surf::diff::Line) -> Self {
|
||||
Self(value.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Addition {
|
||||
pub line: String,
|
||||
pub line_no: u32,
|
||||
pub highlight: Option<crate::syntax::Line>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "diff/")]
|
||||
pub struct Deletion {
|
||||
pub line: String,
|
||||
pub line_no: u32,
|
||||
pub highlight: Option<crate::syntax::Line>,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ pub mod error;
|
||||
pub mod outbound;
|
||||
pub mod repo;
|
||||
pub mod source;
|
||||
pub mod syntax;
|
||||
pub mod test;
|
||||
pub mod traits;
|
||||
|
||||
|
||||
@@ -1,890 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
use tree_sitter_highlight as ts;
|
||||
use ts_rs::TS;
|
||||
|
||||
use radicle::git;
|
||||
use radicle_surf as surf;
|
||||
|
||||
use crate as types;
|
||||
|
||||
/// Shared syntax highlighter. Building the tree-sitter configurations is
|
||||
/// expensive, so we initialize a single `Highlighter` lazily and reuse it
|
||||
/// for the lifetime of the process. Post-construction the highlighter is
|
||||
/// immutable, so no synchronization is needed.
|
||||
pub fn highlighter() -> &'static Highlighter {
|
||||
static HIGHLIGHTER: OnceLock<Highlighter> = OnceLock::new();
|
||||
HIGHLIGHTER.get_or_init(Highlighter::new)
|
||||
}
|
||||
|
||||
/// Highlight groups enabled.
|
||||
const HIGHLIGHTS: &[&str] = &[
|
||||
"attribute",
|
||||
"comment",
|
||||
"comment.documentation",
|
||||
"constant",
|
||||
"constant.builtin",
|
||||
"constructor",
|
||||
"declare",
|
||||
"embedded",
|
||||
"escape",
|
||||
"export",
|
||||
"float.literal",
|
||||
"function",
|
||||
"function.builtin",
|
||||
"function.macro",
|
||||
"function.method",
|
||||
"identifier",
|
||||
"indent.and",
|
||||
"indent.begin",
|
||||
"indent.branch",
|
||||
"indent.end",
|
||||
"integer_literal",
|
||||
"keyword",
|
||||
"keyword.coroutine",
|
||||
"keyword.debug",
|
||||
"keyword.exception",
|
||||
"keyword.repeat",
|
||||
"local.definition",
|
||||
"local.reference",
|
||||
"local.scope",
|
||||
"label",
|
||||
"module",
|
||||
"none",
|
||||
"number",
|
||||
"operator",
|
||||
"property",
|
||||
"punctuation",
|
||||
"punctuation.bracket",
|
||||
"punctuation.delimiter",
|
||||
"punctuation.special",
|
||||
"shorthand_property_identifier",
|
||||
"statement",
|
||||
"string",
|
||||
"string.special",
|
||||
"tag",
|
||||
"tag.delimiter",
|
||||
"tag.error",
|
||||
"text",
|
||||
"text.literal",
|
||||
"text.title",
|
||||
"type",
|
||||
"type.builtin",
|
||||
"type.qualifier",
|
||||
"type_annotation",
|
||||
"variable",
|
||||
"variable.builtin",
|
||||
"variable.parameter",
|
||||
];
|
||||
|
||||
/// A structure encapsulating an item and styling.
|
||||
#[derive(Clone, TS, Debug, Serialize, Eq, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "syntax/")]
|
||||
pub struct Paint {
|
||||
pub item: String,
|
||||
pub style: Option<String>,
|
||||
}
|
||||
|
||||
impl Paint {
|
||||
/// Constructs a new `Paint` structure encapsulating `item` with no set styling.
|
||||
pub fn new(item: String) -> Paint {
|
||||
Paint { item, style: None }
|
||||
}
|
||||
|
||||
/// Sets the style of `self` to `style`.
|
||||
pub fn with_style(mut self, style: String) -> Paint {
|
||||
self.style = Some(style);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A styled string that does not contain any `'\n'`.
|
||||
#[derive(Clone, Debug, Serialize, Eq, PartialEq, TS)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "syntax/")]
|
||||
pub struct Label(Paint);
|
||||
|
||||
impl Label {
|
||||
/// Create a new label.
|
||||
pub fn new(s: &str) -> Self {
|
||||
Self(Paint::new(cleanup(s)))
|
||||
}
|
||||
|
||||
/// Style a label.
|
||||
pub fn style(self, style: String) -> Self {
|
||||
Self(self.0.with_style(style))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Label {
|
||||
fn from(value: String) -> Self {
|
||||
Self::new(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Label {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A line of text that has styling and can be displayed.
|
||||
#[derive(Clone, Debug, Serialize, Default, PartialEq, TS, Eq)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "syntax/")]
|
||||
pub struct Line {
|
||||
items: Vec<Label>,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// Create a new line.
|
||||
pub fn new(item: impl Into<Label>) -> Self {
|
||||
Self {
|
||||
items: vec![item.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Line {
|
||||
type Item = Label;
|
||||
type IntoIter = Box<dyn Iterator<Item = Label>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Box::new(self.items.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Label>> From<T> for Line {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Label>> for Line {
|
||||
fn from(items: Vec<Label>) -> Self {
|
||||
Self { items }
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup the input string for display as a label.
|
||||
fn cleanup(input: &str) -> String {
|
||||
input.chars().filter(|c| *c != '\n' && *c != '\r').collect()
|
||||
}
|
||||
|
||||
/// Syntax highlighted file builder.
|
||||
#[derive(Default)]
|
||||
struct Builder {
|
||||
/// Output lines.
|
||||
lines: Vec<Line>,
|
||||
/// Current output line.
|
||||
line: Vec<Label>,
|
||||
/// Current label.
|
||||
label: Vec<u8>,
|
||||
/// Current stack of styles.
|
||||
styles: Vec<String>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Run the builder to completion.
|
||||
fn run(
|
||||
mut self,
|
||||
highlights: impl Iterator<Item = Result<ts::HighlightEvent, ts::Error>>,
|
||||
code: &[u8],
|
||||
) -> Result<Vec<Line>, ts::Error> {
|
||||
for event in highlights {
|
||||
match event? {
|
||||
ts::HighlightEvent::Source { start, end } => {
|
||||
for (i, byte) in code.iter().enumerate().skip(start).take(end - start) {
|
||||
if *byte == b'\n' {
|
||||
self.advance();
|
||||
// Start on new line.
|
||||
self.lines.push(Line::from(self.line.clone()));
|
||||
self.line.clear();
|
||||
} else if i == code.len() - 1 {
|
||||
// File has no `\n` at the end.
|
||||
self.label.push(*byte);
|
||||
self.advance();
|
||||
self.lines.push(Line::from(self.line.clone()));
|
||||
} else {
|
||||
// Add to existing label.
|
||||
self.label.push(*byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
ts::HighlightEvent::HighlightStart(h) => {
|
||||
let name = HIGHLIGHTS[h.0];
|
||||
|
||||
self.advance();
|
||||
self.styles.push(name.to_string());
|
||||
}
|
||||
ts::HighlightEvent::HighlightEnd => {
|
||||
self.advance();
|
||||
self.styles.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.lines)
|
||||
}
|
||||
|
||||
/// Advance the state by pushing the current label onto the current line,
|
||||
/// using the current styling.
|
||||
fn advance(&mut self) {
|
||||
if !self.label.is_empty() {
|
||||
// Take the top-level style when there are more than one.
|
||||
let style = self.styles.first().cloned().unwrap_or_default();
|
||||
self.line
|
||||
.push(Label::new(String::from_utf8_lossy(&self.label).as_ref()).style(style));
|
||||
self.label.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Syntax highlighter based on `tree-sitter`.
|
||||
pub struct Highlighter {
|
||||
configs: std::collections::HashMap<String, ts::HighlightConfiguration>,
|
||||
}
|
||||
|
||||
impl Default for Highlighter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter {
|
||||
pub fn new() -> Self {
|
||||
let mut configs: std::collections::HashMap<String, ts::HighlightConfiguration> = [
|
||||
("rust", Self::config("rust")),
|
||||
("json", Self::config("json")),
|
||||
("jsdoc", Self::config("jsdoc")),
|
||||
("diff", Self::config("diff")),
|
||||
("typescript", Self::config("typescript")),
|
||||
("javascript", Self::config("javascript")),
|
||||
("markdown", Self::config("markdown")),
|
||||
("css", Self::config("css")),
|
||||
("go", Self::config("go")),
|
||||
("regex", Self::config("regex")),
|
||||
("shell", Self::config("shell")),
|
||||
("c", Self::config("c")),
|
||||
("python", Self::config("python")),
|
||||
("svelte", Self::config("svelte")),
|
||||
("ruby", Self::config("ruby")),
|
||||
("tsx", Self::config("tsx")),
|
||||
("html", Self::config("html")),
|
||||
("toml", Self::config("toml")),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(lang, cfg)| cfg.map(|c| (lang.to_string(), c)))
|
||||
.collect();
|
||||
|
||||
for cfg in configs.values_mut() {
|
||||
cfg.configure(HIGHLIGHTS);
|
||||
}
|
||||
|
||||
Highlighter { configs }
|
||||
}
|
||||
|
||||
/// Highlight a source code file.
|
||||
pub fn highlight(&self, path: &Path, code: &[u8]) -> Result<Vec<Line>, ts::Error> {
|
||||
let mut highlighter = ts::Highlighter::new();
|
||||
// Check for a language if none found return plain lines.
|
||||
let Some(language) = Self::detect(path, code) else {
|
||||
let Ok(code) = std::str::from_utf8(code) else {
|
||||
return Err(ts::Error::Unknown);
|
||||
};
|
||||
return Ok(code.lines().map(Line::new).collect());
|
||||
};
|
||||
|
||||
// Check if there is a configuration if none found return plain lines.
|
||||
let Some(config) = self.configs.get(&language) else {
|
||||
let Ok(code) = std::str::from_utf8(code) else {
|
||||
return Err(ts::Error::Unknown);
|
||||
};
|
||||
return Ok(code.lines().map(Line::new).collect());
|
||||
};
|
||||
|
||||
let configs = &self.configs;
|
||||
let highlights =
|
||||
highlighter.highlight(config, code, None, |language| configs.get(language))?;
|
||||
|
||||
Builder::default().run(highlights, code)
|
||||
}
|
||||
|
||||
/// Detect language.
|
||||
fn detect(path: &Path, _code: &[u8]) -> Option<String> {
|
||||
match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("rs") => Some(String::from("rust")),
|
||||
Some("svelte") => Some(String::from("svelte")),
|
||||
Some("ts" | "js") => Some(String::from("typescript")),
|
||||
Some("json") => Some(String::from("json")),
|
||||
Some("regex") => Some(String::from("regex")),
|
||||
Some("sh" | "bash") => Some(String::from("shell")),
|
||||
Some("md" | "markdown") => Some(String::from("markdown")),
|
||||
Some("go") => Some(String::from("go")),
|
||||
Some("c") => Some(String::from("c")),
|
||||
Some("py") => Some(String::from("python")),
|
||||
Some("rb") => Some(String::from("ruby")),
|
||||
Some("tsx") => Some(String::from("tsx")),
|
||||
Some("html") | Some("htm") | Some("xml") => Some(String::from("html")),
|
||||
Some("css") => Some(String::from("css")),
|
||||
Some("toml") => Some(String::from("toml")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a language configuration.
|
||||
fn config(language: &str) -> Option<ts::HighlightConfiguration> {
|
||||
match language {
|
||||
"rust" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_rust::INJECTIONS_QUERY,
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"diff" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_diff::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_diff::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"json" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_json::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_json::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"javascript" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_javascript::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||
tree_sitter_javascript::INJECTIONS_QUERY,
|
||||
tree_sitter_javascript::LOCALS_QUERY,
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"typescript" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
||||
language,
|
||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
tree_sitter_typescript::LOCALS_QUERY,
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"markdown" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_md::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
|
||||
tree_sitter_md::INJECTION_QUERY_BLOCK,
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"css" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_css::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_css::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"go" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_go::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_go::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"shell" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_bash::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_bash::HIGHLIGHT_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"c" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_c::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_c::HIGHLIGHT_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"python" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_python::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_python::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"regex" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_regex::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_regex::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"svelte" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_svelte_ng::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_svelte_ng::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_svelte_ng::INJECTIONS_QUERY,
|
||||
tree_sitter_svelte_ng::LOCALS_QUERY,
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"ruby" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_ruby::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_ruby::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
tree_sitter_ruby::LOCALS_QUERY,
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"jsdoc" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_jsdoc::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_jsdoc::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"tsx" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_typescript::LANGUAGE_TSX.into(),
|
||||
language,
|
||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_javascript::INJECTIONS_QUERY,
|
||||
tree_sitter_typescript::LOCALS_QUERY,
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"html" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_html::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_html::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_html::INJECTIONS_QUERY,
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
"toml" => Some(
|
||||
ts::HighlightConfiguration::new(
|
||||
tree_sitter_toml_ng::LANGUAGE.into(),
|
||||
language,
|
||||
tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.expect("Highlighter::config: highlight configuration must be valid"),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blob returned by the [`Repo`] trait.
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum Blob {
|
||||
Binary,
|
||||
Empty,
|
||||
Plain(Vec<u8>),
|
||||
}
|
||||
|
||||
/// A repository of Git blobs.
|
||||
pub trait Repo {
|
||||
/// Lookup a blob from the repo.
|
||||
fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error>;
|
||||
/// Lookup a file in the workdir.
|
||||
fn file(&self, path: &Path) -> Option<Blob>;
|
||||
}
|
||||
|
||||
impl Repo for git::raw::Repository {
|
||||
fn blob(&self, oid: git::Oid) -> Result<Blob, git::raw::Error> {
|
||||
let blob = self.find_blob(oid.into())?;
|
||||
|
||||
if blob.is_binary() {
|
||||
Ok(Blob::Binary)
|
||||
} else {
|
||||
let content = blob.content();
|
||||
|
||||
if content.is_empty() {
|
||||
Ok(Blob::Empty)
|
||||
} else {
|
||||
Ok(Blob::Plain(blob.content().to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn file(&self, path: &Path) -> Option<Blob> {
|
||||
self.workdir()
|
||||
.and_then(|dir| fs::read(dir.join(path)).ok())
|
||||
.map(|content| {
|
||||
// A file is considered binary if there is a zero byte in the first 8 kilobytes
|
||||
// of the file. This is the same heuristic Git uses.
|
||||
let binary = content.iter().take(8192).any(|b| *b == 0);
|
||||
if binary {
|
||||
Blob::Binary
|
||||
} else {
|
||||
Blob::Plain(content)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Blobs passed down to the hunk renderer.
|
||||
#[derive(Debug)]
|
||||
pub struct Blobs<T> {
|
||||
pub old: Option<T>,
|
||||
pub new: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Blobs<T> {
|
||||
pub fn new(old: Option<T>, new: Option<T>) -> Self {
|
||||
Self { old, new }
|
||||
}
|
||||
}
|
||||
|
||||
impl Blobs<(PathBuf, Blob)> {
|
||||
pub fn highlight(&self, hi: &Highlighter) -> Blobs<Vec<Line>> {
|
||||
let mut blobs = Blobs::default();
|
||||
if let Some((path, Blob::Plain(content))) = &self.old {
|
||||
blobs.old = hi.highlight(path, content).ok();
|
||||
}
|
||||
if let Some((path, Blob::Plain(content))) = &self.new {
|
||||
blobs.new = hi.highlight(path, content).ok();
|
||||
}
|
||||
blobs
|
||||
}
|
||||
|
||||
pub fn from_paths<R: Repo>(
|
||||
old: Option<(&Path, git::Oid)>,
|
||||
new: Option<(&Path, git::Oid)>,
|
||||
repo: &R,
|
||||
) -> Blobs<(PathBuf, Blob)> {
|
||||
Blobs::new(
|
||||
old.and_then(|(path, oid)| {
|
||||
repo.blob(oid)
|
||||
.ok()
|
||||
.or_else(|| repo.file(path))
|
||||
.map(|blob| (path.to_path_buf(), blob))
|
||||
}),
|
||||
new.and_then(|(path, oid)| {
|
||||
repo.blob(oid)
|
||||
.ok()
|
||||
.or_else(|| repo.file(path))
|
||||
.map(|blob| (path.to_path_buf(), blob))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Blobs<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
old: None,
|
||||
new: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToPretty {
|
||||
/// The output of the render process.
|
||||
type Output: Serialize;
|
||||
/// Context that can be passed down from parent objects during rendering.
|
||||
type Context;
|
||||
|
||||
/// Render to pretty diff output.
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, context: &Self::Context, repo: &R) -> Self::Output;
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Diff {
|
||||
type Output = types::diff::Diff;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, context: &Self::Context, repo: &R) -> Self::Output {
|
||||
let files = self
|
||||
.files()
|
||||
.map(|f| f.pretty(hi, context, repo))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
types::diff::Diff {
|
||||
files,
|
||||
stats: (*self.stats()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::FileDiff {
|
||||
type Output = types::diff::FileDiff;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(
|
||||
&self,
|
||||
hi: &Highlighter,
|
||||
_context: &Self::Context,
|
||||
repo: &R,
|
||||
) -> Self::Output {
|
||||
match self {
|
||||
surf::diff::FileDiff::Added(f) => types::diff::FileDiff::Added(f.pretty(hi, &(), repo)),
|
||||
surf::diff::FileDiff::Deleted(f) => {
|
||||
types::diff::FileDiff::Deleted(f.pretty(hi, &(), repo))
|
||||
}
|
||||
surf::diff::FileDiff::Modified(f) => {
|
||||
types::diff::FileDiff::Modified(f.pretty(hi, &(), repo))
|
||||
}
|
||||
surf::diff::FileDiff::Moved(f) => types::diff::FileDiff::Moved(f.pretty(hi, &(), repo)),
|
||||
surf::diff::FileDiff::Copied(f) => {
|
||||
types::diff::FileDiff::Copied(f.pretty(hi, &(), repo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::DiffContent {
|
||||
type Output = types::diff::DiffContent;
|
||||
type Context = Blobs<(PathBuf, Blob)>;
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, blobs: &Self::Context, repo: &R) -> Self::Output {
|
||||
match self {
|
||||
surf::diff::DiffContent::Plain {
|
||||
hunks: surf::diff::Hunks(hunks),
|
||||
eof,
|
||||
stats,
|
||||
} => {
|
||||
let blobs = blobs.highlight(hi);
|
||||
|
||||
let hunks = hunks
|
||||
.iter()
|
||||
.map(|h| h.pretty(hi, &blobs, repo))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
types::diff::DiffContent::Plain {
|
||||
hunks: hunks.into(),
|
||||
stats: (*stats).into(),
|
||||
eof: (*eof).clone().into(),
|
||||
}
|
||||
}
|
||||
surf::diff::DiffContent::Binary => types::diff::DiffContent::Binary,
|
||||
surf::diff::DiffContent::Empty => types::diff::DiffContent::Empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Moved {
|
||||
type Output = types::diff::Moved;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, _: &Self::Context, repo: &R) -> Self::Output {
|
||||
let old = Some((self.old_path.as_path(), self.old.oid));
|
||||
let new = Some((self.new_path.as_path(), self.new.oid));
|
||||
let blobs = Blobs::from_paths(old, new, repo);
|
||||
|
||||
types::diff::Moved {
|
||||
old_path: self.old_path.clone(),
|
||||
old: self.old.clone().into(),
|
||||
new_path: self.new_path.clone(),
|
||||
new: self.new.clone().into(),
|
||||
diff: self.diff.pretty(hi, &blobs, repo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Added {
|
||||
type Output = types::diff::Added;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, _: &Self::Context, repo: &R) -> Self::Output {
|
||||
let old = None;
|
||||
let new = Some((self.path.as_path(), self.new.oid));
|
||||
let blobs = Blobs::from_paths(old, new, repo);
|
||||
|
||||
types::diff::Added {
|
||||
path: self.path.clone(),
|
||||
diff: self.diff.pretty(hi, &blobs, repo),
|
||||
new: self.new.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Deleted {
|
||||
type Output = types::diff::Deleted;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, _: &Self::Context, repo: &R) -> Self::Output {
|
||||
let old = Some((self.path.as_path(), self.old.oid));
|
||||
let new = None;
|
||||
let blobs = Blobs::from_paths(old, new, repo);
|
||||
|
||||
types::diff::Deleted {
|
||||
path: self.path.clone(),
|
||||
diff: self.diff.pretty(hi, &blobs, repo),
|
||||
old: self.old.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Modified {
|
||||
type Output = types::diff::Modified;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, _: &Self::Context, repo: &R) -> Self::Output {
|
||||
let old = Some((self.path.as_path(), self.old.oid));
|
||||
let new = Some((self.path.as_path(), self.new.oid));
|
||||
let blobs = Blobs::from_paths(old, new, repo);
|
||||
|
||||
types::diff::Modified {
|
||||
path: self.path.clone(),
|
||||
diff: self.diff.pretty(hi, &blobs, repo),
|
||||
new: self.new.clone().into(),
|
||||
old: self.old.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Copied {
|
||||
type Output = types::diff::Copied;
|
||||
type Context = ();
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, _: &Self::Context, repo: &R) -> Self::Output {
|
||||
let old = Some((self.old_path.as_path(), self.old.oid));
|
||||
let new = Some((self.new_path.as_path(), self.new.oid));
|
||||
let blobs = Blobs::from_paths(old, new, repo);
|
||||
|
||||
types::diff::Copied {
|
||||
old_path: self.old_path.clone(),
|
||||
new_path: self.new_path.clone(),
|
||||
diff: self.diff.pretty(hi, &blobs, repo),
|
||||
new: self.new.clone().into(),
|
||||
old: self.old.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Hunk<surf::diff::Modification> {
|
||||
type Output = types::diff::Hunk;
|
||||
type Context = Blobs<Vec<Line>>;
|
||||
|
||||
fn pretty<R: Repo>(&self, hi: &Highlighter, blobs: &Self::Context, repo: &R) -> Self::Output {
|
||||
let lines = self
|
||||
.lines
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|l| l.pretty(hi, blobs, repo))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
types::diff::Hunk {
|
||||
header: String::from_utf8_lossy(self.header.as_bytes()).to_string(),
|
||||
new: self.new.clone(),
|
||||
old: self.old.clone(),
|
||||
lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPretty for surf::diff::Modification {
|
||||
type Output = types::diff::Modification;
|
||||
type Context = Blobs<Vec<Line>>;
|
||||
|
||||
fn pretty<R: Repo>(
|
||||
&self,
|
||||
_hi: &Highlighter,
|
||||
blobs: &<radicle_surf::diff::Modification as ToPretty>::Context,
|
||||
_repo: &R,
|
||||
) -> Self::Output {
|
||||
match self {
|
||||
surf::diff::Modification::Deletion(surf::diff::Deletion { line, line_no }) => {
|
||||
if let Some(lines) = &blobs.old.as_ref() {
|
||||
types::diff::Modification::Deletion(types::diff::Deletion {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
highlight: Some(lines[*line_no as usize - 1].clone()),
|
||||
line_no: *line_no,
|
||||
})
|
||||
} else {
|
||||
types::diff::Modification::Deletion(types::diff::Deletion {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no: *line_no,
|
||||
highlight: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
surf::diff::Modification::Addition(surf::diff::Addition { line, line_no }) => {
|
||||
if let Some(lines) = &blobs.new.as_ref() {
|
||||
types::diff::Modification::Addition(types::diff::Addition {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no: *line_no,
|
||||
highlight: Some(lines[*line_no as usize - 1].clone()),
|
||||
})
|
||||
} else {
|
||||
types::diff::Modification::Addition(types::diff::Addition {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no: *line_no,
|
||||
highlight: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
surf::diff::Modification::Context {
|
||||
line,
|
||||
line_no_new,
|
||||
line_no_old,
|
||||
} => {
|
||||
// Nb. we can check in the old or the new blob, we choose the new.
|
||||
if let Some(lines) = &blobs.new.as_ref() {
|
||||
types::diff::Modification::Context {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no_new: *line_no_new,
|
||||
line_no_old: *line_no_old,
|
||||
highlight: Some(lines[*line_no_new as usize - 1].clone()),
|
||||
}
|
||||
} else {
|
||||
types::diff::Modification::Context {
|
||||
line: String::from_utf8_lossy(line.as_bytes()).to_string(),
|
||||
line_no_new: *line_no_new,
|
||||
line_no_old: *line_no_old,
|
||||
highlight: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user