Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add syntax highlighting for diffs
Open did:key:z6MkkfM3...sVz5 opened 1 year ago

Use ts_rs format feature instead of prettier

checkcheck-e2e

👉 Workflow runs 👉 Branch on GitHub

47 files changed +2665 -138 52c25a90 cc28740d
modified Cargo.lock
@@ -3,6 +3,16 @@
version = 3

[[package]]
+
name = "Inflector"
+
version = "0.11.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
dependencies = [
+
 "lazy_static",
+
 "regex",
+
]
+

+
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -70,6 +80,18 @@ dependencies = [
]

[[package]]
+
name = "ahash"
+
version = "0.8.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+
dependencies = [
+
 "cfg-if",
+
 "once_cell",
+
 "version_check",
+
 "zerocopy",
+
]
+

+
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -100,6 +122,12 @@ dependencies = [
]

[[package]]
+
name = "allocator-api2"
+
version = "0.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+

+
[[package]]
name = "amplify"
version = "4.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -229,6 +257,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"

[[package]]
+
name = "ast_node"
+
version = "0.9.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "swc_macros_common",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "async-trait"
version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -407,6 +447,15 @@ dependencies = [
]

[[package]]
+
name = "better_scoped_tls"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7"
+
dependencies = [
+
 "scoped-tls",
+
]
+

+
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -544,6 +593,9 @@ name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
dependencies = [
+
 "allocator-api2",
+
]

[[package]]
name = "byte-unit"
@@ -683,9 +735,9 @@ dependencies = [

[[package]]
name = "cc"
-
version = "1.1.31"
+
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
+
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [
 "jobserver",
 "libc",
@@ -1140,6 +1192,56 @@ dependencies = [
]

[[package]]
+
name = "data-url"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
+

+
[[package]]
+
name = "deno_ast"
+
version = "0.38.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "584547d27786a734536fde7088f8429d355569c39410427be44695c300618408"
+
dependencies = [
+
 "deno_media_type",
+
 "deno_terminal",
+
 "dprint-swc-ext",
+
 "once_cell",
+
 "percent-encoding",
+
 "serde",
+
 "swc_atoms",
+
 "swc_common",
+
 "swc_ecma_ast",
+
 "swc_ecma_parser",
+
 "swc_eq_ignore_macros",
+
 "text_lines",
+
 "thiserror",
+
 "unicode-width",
+
 "url",
+
]
+

+
[[package]]
+
name = "deno_media_type"
+
version = "0.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a8978229b82552bf8457a0125aa20863f023619cfc21ebb007b1e571d68fd85b"
+
dependencies = [
+
 "data-url",
+
 "serde",
+
 "url",
+
]
+

+
[[package]]
+
name = "deno_terminal"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7e6337d4e7f375f8b986409a76fbeecfa4bd8a1343e63355729ae4befa058eaf"
+
dependencies = [
+
 "once_cell",
+
 "termcolor",
+
]
+

+
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1244,6 +1346,63 @@ dependencies = [
]

[[package]]
+
name = "dprint-core"
+
version = "0.66.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f3ab0dd2bedc109d25f0d21afb09b7d329f6c6fa83b095daf31d2d967e091548"
+
dependencies = [
+
 "anyhow",
+
 "bumpalo",
+
 "hashbrown 0.14.5",
+
 "indexmap 2.6.0",
+
 "rustc-hash",
+
 "serde",
+
 "unicode-width",
+
]
+

+
[[package]]
+
name = "dprint-core-macros"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1675ad2b358481f3cc46202040d64ac7a36c4ade414a696df32e0e45421a6e9f"
+
dependencies = [
+
 "quote",
+
 "syn 1.0.109",
+
]
+

+
[[package]]
+
name = "dprint-plugin-typescript"
+
version = "0.90.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d7c3c339020ebbbbbe5fc049350935ee2ea2ba5a3fc01f753588639a30404cda"
+
dependencies = [
+
 "anyhow",
+
 "deno_ast",
+
 "dprint-core",
+
 "dprint-core-macros",
+
 "percent-encoding",
+
 "rustc-hash",
+
 "serde",
+
]
+

+
[[package]]
+
name = "dprint-swc-ext"
+
version = "0.16.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "019d17f2c2457c5a70a7cf4505b1a562ca8ab168c0ac0c005744efbd29fcb8fe"
+
dependencies = [
+
 "allocator-api2",
+
 "bumpalo",
+
 "num-bigint",
+
 "rustc-hash",
+
 "swc_atoms",
+
 "swc_common",
+
 "swc_ecma_ast",
+
 "swc_ecma_parser",
+
 "text_lines",
+
]
+

+
[[package]]
name = "dtoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1543,6 +1702,17 @@ dependencies = [
]

[[package]]
+
name = "from_variant"
+
version = "0.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4"
+
dependencies = [
+
 "proc-macro2",
+
 "swc_macros_common",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2027,7 +2197,17 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
-
 "ahash",
+
 "ahash 0.7.8",
+
]
+

+
[[package]]
+
name = "hashbrown"
+
version = "0.14.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
dependencies = [
+
 "ahash 0.8.11",
+
 "allocator-api2",
]

[[package]]
@@ -2070,6 +2250,20 @@ dependencies = [
]

[[package]]
+
name = "hstr"
+
version = "0.2.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dae404c0c5d4e95d4858876ab02eecd6a196bb8caa42050dfa809938833fc412"
+
dependencies = [
+
 "hashbrown 0.14.5",
+
 "new_debug_unreachable",
+
 "once_cell",
+
 "phf 0.11.2",
+
 "rustc-hash",
+
 "triomphe",
+
]
+

+
[[package]]
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2333,6 +2527,18 @@ dependencies = [
]

[[package]]
+
name = "is-macro"
+
version = "0.3.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4"
+
dependencies = [
+
 "heck 0.5.0",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2858,6 +3064,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
 "num-integer",
 "num-traits",
+
 "serde",
]

[[package]]
@@ -3693,6 +3900,15 @@ dependencies = [
]

[[package]]
+
name = "psm"
+
version = "0.1.24"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
+
dependencies = [
+
 "cc",
+
]
+

+
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3906,6 +4122,21 @@ dependencies = [
 "serde",
 "serde_json",
 "thiserror",
+
 "tree-sitter-bash",
+
 "tree-sitter-c",
+
 "tree-sitter-css",
+
 "tree-sitter-go",
+
 "tree-sitter-highlight",
+
 "tree-sitter-html",
+
 "tree-sitter-javascript",
+
 "tree-sitter-json",
+
 "tree-sitter-md",
+
 "tree-sitter-python",
+
 "tree-sitter-ruby",
+
 "tree-sitter-rust",
+
 "tree-sitter-svelte-ng",
+
 "tree-sitter-toml-ng",
+
 "tree-sitter-typescript",
 "ts-rs",
]

@@ -4255,6 +4486,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"

[[package]]
+
name = "rustc-hash"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+

+
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4325,6 +4562,12 @@ dependencies = [
]

[[package]]
+
name = "scoped-tls"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+

+
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4640,6 +4883,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"

[[package]]
+
name = "smartstring"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
+
dependencies = [
+
 "autocfg",
+
 "static_assertions",
+
 "version_check",
+
]
+

+
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4812,6 +5066,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"

[[package]]
+
name = "stacker"
+
version = "0.1.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
+
dependencies = [
+
 "cc",
+
 "cfg-if",
+
 "libc",
+
 "psm",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "static_assertions"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+

+
[[package]]
+
name = "streaming-iterator"
+
version = "0.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
+

+
[[package]]
name = "string_cache"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4838,6 +5117,18 @@ dependencies = [
]

[[package]]
+
name = "string_enum"
+
version = "0.4.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "swc_macros_common",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4850,6 +5141,128 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"

[[package]]
+
name = "swc_atoms"
+
version = "0.6.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125"
+
dependencies = [
+
 "hstr",
+
 "once_cell",
+
 "rustc-hash",
+
 "serde",
+
]
+

+
[[package]]
+
name = "swc_common"
+
version = "0.33.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a2f9706038906e66f3919028f9f7a37f3ed552f1b85578e93f4468742e2da438"
+
dependencies = [
+
 "ast_node",
+
 "better_scoped_tls",
+
 "cfg-if",
+
 "either",
+
 "from_variant",
+
 "new_debug_unreachable",
+
 "num-bigint",
+
 "once_cell",
+
 "rustc-hash",
+
 "serde",
+
 "siphasher 0.3.11",
+
 "swc_atoms",
+
 "swc_eq_ignore_macros",
+
 "swc_visit",
+
 "tracing",
+
 "unicode-width",
+
 "url",
+
]
+

+
[[package]]
+
name = "swc_ecma_ast"
+
version = "0.113.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dc1690cc0c9ab60b44ac0225ba1e231ac532f7ba1d754df761c6ee607561afae"
+
dependencies = [
+
 "bitflags 2.6.0",
+
 "is-macro",
+
 "num-bigint",
+
 "phf 0.11.2",
+
 "scoped-tls",
+
 "serde",
+
 "string_enum",
+
 "swc_atoms",
+
 "swc_common",
+
 "unicode-id-start",
+
]
+

+
[[package]]
+
name = "swc_ecma_parser"
+
version = "0.144.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0499e69683ae5d67a20ff0279b94bc90f29df7922a46331b54d5dd367bf89570"
+
dependencies = [
+
 "either",
+
 "new_debug_unreachable",
+
 "num-bigint",
+
 "num-traits",
+
 "phf 0.11.2",
+
 "serde",
+
 "smallvec",
+
 "smartstring",
+
 "stacker",
+
 "swc_atoms",
+
 "swc_common",
+
 "swc_ecma_ast",
+
 "tracing",
+
 "typed-arena",
+
]
+

+
[[package]]
+
name = "swc_eq_ignore_macros"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "695a1d8b461033d32429b5befbf0ad4d7a2c4d6ba9cd5ba4e0645c615839e8e4"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
+
name = "swc_macros_common"
+
version = "0.3.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "27e18fbfe83811ffae2bb23727e45829a0d19c6870bced7c0f545cc99ad248dd"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
+
name = "swc_visit"
+
version = "0.5.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "043d11fe683dcb934583ead49405c0896a5af5face522e4682c16971ef7871b9"
+
dependencies = [
+
 "either",
+
 "swc_visit_macros",
+
]
+

+
[[package]]
+
name = "swc_visit_macros"
+
version = "0.5.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "92807d840959f39c60ce8a774a3f83e8193c658068e6d270dbe0a05e40e90b41"
+
dependencies = [
+
 "Inflector",
+
 "proc-macro2",
+
 "quote",
+
 "swc_macros_common",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "swift-rs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5347,6 +5760,15 @@ dependencies = [
]

[[package]]
+
name = "text_lines"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7fd5828de7deaa782e1dd713006ae96b3bee32d3279b79eb67ecf8072c059bcf"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
name = "thin-slice"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5580,10 +6002,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
 "pin-project-lite",
+
 "tracing-attributes",
 "tracing-core",
]

[[package]]
+
name = "tracing-attributes"
+
version = "0.1.28"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.81",
+
]
+

+
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5614,6 +6048,188 @@ dependencies = [
]

[[package]]
+
name = "tree-sitter"
+
version = "0.24.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
+
dependencies = [
+
 "cc",
+
 "regex",
+
 "regex-syntax",
+
 "streaming-iterator",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-bash"
+
version = "0.23.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-c"
+
version = "0.23.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "db56fadd8c3c6bc880dffcf1177c9d1c54a71a5207716db8660189082e63b587"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-css"
+
version = "0.23.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "25435a275adb3226b6fddab891bbc50d1a500774a44ceb97022a39666ccda75d"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-go"
+
version = "0.23.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-highlight"
+
version = "0.24.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7f0f856de10d70a6d14d66db3648f7410c131cd49e989a863f15dda9acae6044"
+
dependencies = [
+
 "lazy_static",
+
 "regex",
+
 "streaming-iterator",
+
 "thiserror",
+
 "tree-sitter",
+
]
+

+
[[package]]
+
name = "tree-sitter-html"
+
version = "0.23.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-javascript"
+
version = "0.23.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-json"
+
version = "0.24.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-language"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600"
+

+
[[package]]
+
name = "tree-sitter-md"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "17f968c22a01010b83fc960455ae729db08dbeb6388617d9113897cb9204b030"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-python"
+
version = "0.23.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2416de7eea3f2e1bd53c250f2d3f3394fc77f78497680f37f4b87918b8d752e3"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-ruby"
+
version = "0.23.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-rust"
+
version = "0.23.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4d64d449ca63e683c562c7743946a646671ca23947b9c925c0cfbe65051a4af"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-svelte-ng"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-toml-ng"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e9adc2c898ae49730e857d75be403da3f92bb81d8e37a2f918a08dd10de5ebb1"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "tree-sitter-typescript"
+
version = "0.23.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
+
dependencies = [
+
 "cc",
+
 "tree-sitter-language",
+
]
+

+
[[package]]
+
name = "triomphe"
+
version = "0.1.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85"
+
dependencies = [
+
 "serde",
+
 "stable_deref_trait",
+
]
+

+
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5625,6 +6241,7 @@ version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
dependencies = [
+
 "dprint-plugin-typescript",
 "lazy_static",
 "serde_json",
 "thiserror",
@@ -5644,6 +6261,12 @@ dependencies = [
]

[[package]]
+
name = "typed-arena"
+
version = "2.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
+

+
[[package]]
name = "typeid"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5703,6 +6326,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"

[[package]]
+
name = "unicode-id-start"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "02aebfa694eccbbbffdd92922c7de136b9fe764396d2f10e21bce1681477cfc1"
+

+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5724,6 +6353,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"

[[package]]
+
name = "unicode-width"
+
version = "0.1.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+

+
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6063,7 +6698,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
-
 "windows-sys 0.48.0",
+
 "windows-sys 0.59.0",
]

[[package]]
modified crates/radicle-tauri/src/commands/diff.rs
@@ -1,6 +1,5 @@
-
use radicle_surf as surf;
-

use radicle::identity;
+
use radicle_types as types;
use radicle_types::error::Error;
use radicle_types::traits::repo::Repo;

@@ -11,6 +10,6 @@ pub async fn get_diff(
    ctx: tauri::State<'_, AppState>,
    rid: identity::RepoId,
    options: radicle_types::cobs::diff::Options,
-
) -> Result<surf::diff::Diff, Error> {
+
) -> Result<types::diff::Diff, Error> {
    ctx.get_diff(rid, options)
}
modified crates/radicle-types/Cargo.toml
@@ -7,10 +7,25 @@ edition = "2021"
anyhow = { version = "1.0.90" }
axum = { version = "0.7.5", default-features = false, features = ["json"] }
base64 = { version = "0.22.1" }
+
localtime = { version = "1.3.1" }
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
thiserror = { version = "1.0.65" }
-
ts-rs = { version = "10.0.0", features = [ "serde-json-impl", "no-serde-warnings" ] }
-
localtime = { version = "1.3.1" }
+
tree-sitter-bash = { version = "0.23.3" }
+
tree-sitter-c = { version = "0.23.2" }
+
tree-sitter-css = { version = "0.23.1" }
+
tree-sitter-go = { version = "0.23.4" }
+
tree-sitter-highlight = { version = "0.24.4" }
+
tree-sitter-html = { version = "0.23.2" }
+
tree-sitter-javascript = { version = "0.23.1" }
+
tree-sitter-json = { version = "0.24.8" }
+
tree-sitter-md = { version = "0.3.2" }
+
tree-sitter-python = { version = "0.23.4" }
+
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 = "10.0.0", features = [ "format", "serde-json-impl", "no-serde-warnings" ] }
modified crates/radicle-types/bindings/cob/Never.ts
@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
-
 *A type alias for the TS type `never`.
+
 * A type alias for the TS type `never`.
 */
export type Never = never;
modified crates/radicle-types/bindings/cob/issue/Action.ts
@@ -3,11 +3,11 @@ import type { Embed } from "../thread/Embed";
import type { State } from "./State";

export type Action =
-
  | { type: "assign"; assignees: Array<string> }
-
  | { type: "edit"; title: string }
-
  | { type: "lifecycle"; state: State }
-
  | { type: "label"; labels: Array<string> }
-
  | { type: "comment"; body: string; replyTo?: string; embeds?: Array<Embed> }
-
  | { type: "comment.edit"; id: string; body: string; embeds?: Array<Embed> }
-
  | { type: "comment.redact"; id: string }
-
  | { type: "comment.react"; id: string; reaction: string; active: boolean };
+
  | { "type": "assign"; assignees: Array<string> }
+
  | { "type": "edit"; title: string }
+
  | { "type": "lifecycle"; state: State }
+
  | { "type": "label"; labels: Array<string> }
+
  | { "type": "comment"; body: string; replyTo?: string; embeds?: Array<Embed> }
+
  | { "type": "comment.edit"; id: string; body: string; embeds?: Array<Embed> }
+
  | { "type": "comment.redact"; id: string }
+
  | { "type": "comment.react"; id: string; reaction: string; active: boolean };
modified crates/radicle-types/bindings/cob/issue/Operation.ts
@@ -3,17 +3,25 @@ import type { Author } from "../Author";
import type { Embed } from "../thread/Embed";
import type { State } from "./State";

-
export type Operation = {
-
  entryId: string;
-
  timestamp: number;
-
  author: Author;
-
} & (
-
  | { type: "assign"; assignees: Array<string> }
-
  | { type: "edit"; title: string }
-
  | { type: "lifecycle"; state: State }
-
  | { type: "label"; labels: Array<string> }
-
  | { type: "comment"; body: string; replyTo?: string; embeds?: Array<Embed> }
-
  | { type: "comment.edit"; id: string; body: string; embeds?: Array<Embed> }
-
  | { type: "comment.redact"; id: string }
-
  | { type: "comment.react"; id: string; reaction: string; active: boolean }
-
);
+
export type Operation =
+
  & { entryId: string; timestamp: number; author: Author }
+
  & (
+
    | { "type": "assign"; assignees: Array<string> }
+
    | { "type": "edit"; title: string }
+
    | { "type": "lifecycle"; state: State }
+
    | { "type": "label"; labels: Array<string> }
+
    | {
+
      "type": "comment";
+
      body: string;
+
      replyTo?: string;
+
      embeds?: Array<Embed>;
+
    }
+
    | {
+
      "type": "comment.edit";
+
      id: string;
+
      body: string;
+
      embeds?: Array<Embed>;
+
    }
+
    | { "type": "comment.redact"; id: string }
+
    | { "type": "comment.react"; id: string; reaction: string; active: boolean }
+
  );
modified crates/radicle-types/bindings/cob/issue/State.ts
@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CloseReason } from "./CloseReason";

-
export type State =
-
  | { status: "closed"; reason: CloseReason }
-
  | { status: "open" };
+
export type State = { "status": "closed"; reason: CloseReason } | {
+
  "status": "open";
+
};
modified crates/radicle-types/bindings/cob/patch/Action.ts
@@ -3,92 +3,92 @@ import type { CodeLocation } from "../thread/CodeLocation";
import type { Embed } from "../thread/Embed";

export type Action =
-
  | { type: "edit"; title: string; target: string }
-
  | { type: "label"; labels: Array<string> }
-
  | { type: "lifecycle"; state: { status: "draft" | "open" | "archived" } }
-
  | { type: "assign"; assignees: Array<string> }
-
  | { type: "merge"; revision: string; commit: string }
+
  | { "type": "edit"; title: string; target: string }
+
  | { "type": "label"; labels: Array<string> }
+
  | { "type": "lifecycle"; state: { status: "draft" | "open" | "archived" } }
+
  | { "type": "assign"; assignees: Array<string> }
+
  | { "type": "merge"; revision: string; commit: string }
  | {
-
      type: "review";
-
      revision: string;
-
      summary?: string;
-
      verdict?: string;
-
      labels?: Array<string>;
-
    }
+
    "type": "review";
+
    revision: string;
+
    summary?: string;
+
    verdict?: string;
+
    labels?: Array<string>;
+
  }
  | {
-
      type: "review.edit";
-
      review: string;
-
      summary?: string;
-
      verdict?: string;
-
      labels?: Array<string>;
-
    }
-
  | { type: "review.redact"; review: string }
+
    "type": "review.edit";
+
    review: string;
+
    summary?: string;
+
    verdict?: string;
+
    labels?: Array<string>;
+
  }
+
  | { "type": "review.redact"; review: string }
  | {
-
      type: "review.comment";
-
      review: string;
-
      body: string;
-
      location?: CodeLocation;
-
      reply_to?: string;
-
      embeds?: Array<Embed>;
-
    }
+
    "type": "review.comment";
+
    review: string;
+
    body: string;
+
    location?: CodeLocation;
+
    reply_to?: string;
+
    embeds?: Array<Embed>;
+
  }
  | {
-
      type: "review.comment.edit";
-
      review: string;
-
      comment: string;
-
      body: string;
-
      embeds?: Array<Embed>;
-
    }
-
  | { type: "review.comment.redact"; review: string; comment: string }
+
    "type": "review.comment.edit";
+
    review: string;
+
    comment: string;
+
    body: string;
+
    embeds?: Array<Embed>;
+
  }
+
  | { "type": "review.comment.redact"; review: string; comment: string }
  | {
-
      type: "review.comment.react";
-
      review: string;
-
      comment: string;
-
      reaction: string;
-
      active: boolean;
-
    }
-
  | { type: "review.comment.resolve"; review: string; comment: string }
-
  | { type: "review.comment.unresolve"; review: string; comment: string }
+
    "type": "review.comment.react";
+
    review: string;
+
    comment: string;
+
    reaction: string;
+
    active: boolean;
+
  }
+
  | { "type": "review.comment.resolve"; review: string; comment: string }
+
  | { "type": "review.comment.unresolve"; review: string; comment: string }
  | {
-
      type: "revision";
-
      description: string;
-
      base: string;
-
      oid: string;
-
      resolves?: Array<[string, string]>;
-
    }
+
    "type": "revision";
+
    description: string;
+
    base: string;
+
    oid: string;
+
    resolves?: Array<[string, string]>;
+
  }
  | {
-
      type: "revision.edit";
-
      revision: string;
-
      description: string;
-
      embeds?: Array<Embed>;
-
    }
+
    "type": "revision.edit";
+
    revision: string;
+
    description: string;
+
    embeds?: Array<Embed>;
+
  }
  | {
-
      type: "revision.react";
-
      revision: string;
-
      location?: CodeLocation;
-
      reaction: string;
-
      active: boolean;
-
    }
-
  | { type: "revision.redact"; revision: string }
+
    "type": "revision.react";
+
    revision: string;
+
    location?: CodeLocation;
+
    reaction: string;
+
    active: boolean;
+
  }
+
  | { "type": "revision.redact"; revision: string }
  | {
-
      type: "revision.comment";
-
      revision: string;
-
      location?: CodeLocation;
-
      body: string;
-
      replyTo?: string;
-
      embeds?: Array<Embed>;
-
    }
+
    "type": "revision.comment";
+
    revision: string;
+
    location?: CodeLocation;
+
    body: string;
+
    replyTo?: string;
+
    embeds?: Array<Embed>;
+
  }
  | {
-
      type: "revision.comment.edit";
-
      revision: string;
-
      comment: string;
-
      body: string;
-
      embeds?: Array<Embed>;
-
    }
-
  | { type: "revision.comment.redact"; revision: string; comment: string }
+
    "type": "revision.comment.edit";
+
    revision: string;
+
    comment: string;
+
    body: string;
+
    embeds?: Array<Embed>;
+
  }
+
  | { "type": "revision.comment.redact"; revision: string; comment: string }
  | {
-
      type: "revision.comment.react";
-
      revision: string;
-
      comment: string;
-
      reaction: string;
-
      active: boolean;
-
    };
+
    "type": "revision.comment.react";
+
    revision: string;
+
    comment: string;
+
    reaction: string;
+
    active: boolean;
+
  };
modified crates/radicle-types/bindings/cob/patch/State.ts
@@ -1,7 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type State =
-
  | { status: "draft" }
-
  | { status: "open"; conflicts?: Array<[string, string]> }
-
  | { status: "archived" }
-
  | { status: "merged"; revision: string; commit: string };
+
  | { "status": "draft" }
+
  | { "status": "open"; conflicts?: Array<[string, string]> }
+
  | { "status": "archived" }
+
  | { "status": "merged"; revision: string; commit: string };
modified crates/radicle-types/bindings/cob/thread/CodeRange.ts
@@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

-
export type CodeRange =
-
  | { type: "lines"; range: { start: number; end: number } }
-
  | { type: "chars"; line: number; range: { start: number; end: number } };
+
export type CodeRange = {
+
  "type": "lines";
+
  range: { start: number; end: number };
+
} | { "type": "chars"; line: number; range: { start: number; end: number } };
modified crates/radicle-types/bindings/config/Config.ts
@@ -15,7 +15,7 @@ export type Config = {
  /**
   * Default seeding policy.
   */
-
  seedingPolicy:
-
    | { default: "allow"; scope: "followed" | "all" }
-
    | { default: "block" };
+
  seedingPolicy: { default: "allow"; scope: "followed" | "all" } | {
+
    default: "block";
+
  };
};
added crates/radicle-types/bindings/diff/Added.ts
@@ -0,0 +1,5 @@
+
// 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 };
added crates/radicle-types/bindings/diff/Addition.ts
@@ -0,0 +1,4 @@
+
// 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 };
added crates/radicle-types/bindings/diff/Copied.ts
@@ -0,0 +1,11 @@
+
// 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;
+
};
added crates/radicle-types/bindings/diff/Deleted.ts
@@ -0,0 +1,5 @@
+
// 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 };
added crates/radicle-types/bindings/diff/Deletion.ts
@@ -0,0 +1,4 @@
+
// 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 };
added crates/radicle-types/bindings/diff/Diff.ts
@@ -0,0 +1,5 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { FileDiff } from "./FileDiff";
+
import type { Stats } from "./Stats";
+

+
export type Diff = { files: Array<FileDiff>; stats: Stats };
added crates/radicle-types/bindings/diff/DiffContent.ts
@@ -0,0 +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";
+

+
export type DiffContent = { "type": "binary" } | {
+
  "type": "plain";
+
  hunks: Hunks;
+
  stats: FileStats;
+
  eof: EofNewLine;
+
} | { "type": "empty" };
added crates/radicle-types/bindings/diff/DiffFile.ts
@@ -0,0 +1,4 @@
+
// 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 };
added crates/radicle-types/bindings/diff/EofNewLine.ts
@@ -0,0 +1,7 @@
+
// 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";
added crates/radicle-types/bindings/diff/FileDiff.ts
@@ -0,0 +1,13 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Added } from "./Added";
+
import type { Copied } from "./Copied";
+
import type { Deleted } from "./Deleted";
+
import type { Modified } from "./Modified";
+
import type { Moved } from "./Moved";
+

+
export type FileDiff =
+
  | { "status": "added" } & Added
+
  | { "status": "deleted" } & Deleted
+
  | { "status": "modified" } & Modified
+
  | { "status": "moved" } & Moved
+
  | { "status": "copied" } & Copied;
added crates/radicle-types/bindings/diff/FileMode.ts
@@ -0,0 +1,3 @@
+
// 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";
added crates/radicle-types/bindings/diff/FileStats.ts
@@ -0,0 +1,3 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type FileStats = { additions: number; deletions: number };
added crates/radicle-types/bindings/diff/Hunk.ts
@@ -0,0 +1,9 @@
+
// 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 };
+
};
added crates/radicle-types/bindings/diff/Hunks.ts
@@ -0,0 +1,4 @@
+
// 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>;
added crates/radicle-types/bindings/diff/Line.ts
@@ -0,0 +1,3 @@
+
// 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>;
added crates/radicle-types/bindings/diff/Modification.ts
@@ -0,0 +1,15 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Addition } from "./Addition";
+
import type { Deletion } from "./Deletion";
+
import type { Line } from "../syntax/Line";
+

+
export type Modification =
+
  | { "type": "addition" } & Addition
+
  | { "type": "deletion" } & Deletion
+
  | {
+
    "type": "context";
+
    line: string;
+
    lineNoOld: number;
+
    lineNoNew: number;
+
    highlight: Line | null;
+
  };
added crates/radicle-types/bindings/diff/Modified.ts
@@ -0,0 +1,10 @@
+
// 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;
+
};
added crates/radicle-types/bindings/diff/Moved.ts
@@ -0,0 +1,11 @@
+
// 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;
+
};
added crates/radicle-types/bindings/diff/Stats.ts
@@ -0,0 +1,7 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type Stats = {
+
  filesChanged: number;
+
  insertions: number;
+
  deletions: number;
+
};
modified crates/radicle-types/bindings/repo/Visibility.ts
@@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

-
export type Visibility =
-
  | { type: "public" }
-
  | { type: "private"; allow?: Array<string> };
+
export type Visibility = { "type": "public" } | {
+
  "type": "private";
+
  allow?: Array<string>;
+
};
added crates/radicle-types/bindings/syntax/Label.ts
@@ -0,0 +1,7 @@
+
// 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;
added crates/radicle-types/bindings/syntax/Line.ts
@@ -0,0 +1,7 @@
+
// 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> };
added crates/radicle-types/bindings/syntax/Paint.ts
@@ -0,0 +1,6 @@
+
// 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 };
modified crates/radicle-types/src/cobs/diff.rs
@@ -5,5 +5,6 @@ use serde::{Deserialize, Serialize};
pub struct Options {
    pub base: git::Oid,
    pub head: git::Oid,
-
    pub unified: u32,
+
    pub unified: Option<u32>,
+
    pub highlight: Option<bool>,
}
added crates/radicle-types/src/diff.rs
@@ -0,0 +1,415 @@
+
use std::{ops::Range, 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/")]
+
pub struct Diff {
+
    pub files: Vec<FileDiff>,
+
    pub stats: Stats,
+
}
+

+
impl From<surf::diff::Diff> for Diff {
+
    fn from(value: surf::diff::Diff) -> Self {
+
        Self {
+
            files: value.files().cloned().map(Into::into).collect::<Vec<_>>(),
+
            stats: (*value.stats()).into(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(
+
    tag = "status",
+
    rename_all_fields = "camelCase",
+
    rename_all = "camelCase"
+
)]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub enum FileDiff {
+
    Added(Added),
+
    Deleted(Deleted),
+
    Modified(Modified),
+
    Moved(Moved),
+
    Copied(Copied),
+
}
+

+
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 }) => {
+
                Self::Added(Added {
+
                    path,
+
                    diff: diff.into(),
+
                    new: new.into(),
+
                })
+
            }
+
            surf::diff::FileDiff::Deleted(surf::diff::Deleted { path, diff, old }) => {
+
                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::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(),
+
            }),
+
        }
+
    }
+
}
+

+
#[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 DiffContent {
+
    Binary,
+
    Plain {
+
        hunks: Hunks,
+
        stats: FileStats,
+
        eof: EofNewLine,
+
    },
+
    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(),
+
                stats: stats.into(),
+
                eof: eof.into(),
+
            },
+
            surf::diff::DiffContent::Binary => Self::Binary,
+
            surf::diff::DiffContent::Empty => Self::Empty,
+
        }
+
    }
+
}
+

+
#[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)]
+
#[ts(export_to = "diff/")]
+
pub struct Added {
+
    pub path: PathBuf,
+
    pub diff: DiffContent,
+
    pub new: DiffFile,
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub struct Deleted {
+
    pub path: PathBuf,
+
    pub diff: DiffContent,
+
    pub old: DiffFile,
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub struct Moved {
+
    pub old_path: PathBuf,
+
    pub old: DiffFile,
+
    pub new_path: PathBuf,
+
    pub new: DiffFile,
+
    pub diff: DiffContent,
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub struct Copied {
+
    pub old_path: PathBuf,
+
    pub new_path: PathBuf,
+
    pub old: DiffFile,
+
    pub new: DiffFile,
+
    pub diff: DiffContent,
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub struct Modified {
+
    pub path: PathBuf,
+
    pub diff: DiffContent,
+
    pub old: DiffFile,
+
    pub new: DiffFile,
+
}
+

+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "diff/")]
+
pub struct Stats {
+
    pub files_changed: usize,
+
    pub insertions: usize,
+
    pub deletions: usize,
+
}
+

+
impl From<surf::diff::Stats> for Stats {
+
    fn from(value: surf::diff::Stats) -> Self {
+
        Self {
+
            files_changed: value.files_changed,
+
            insertions: value.insertions,
+
            deletions: value.deletions,
+
        }
+
    }
+
}
+

+
#[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)]
+
#[ts(export_to = "diff/")]
+
pub struct FileStats {
+
    pub additions: usize,
+
    pub deletions: usize,
+
}
+

+
impl From<surf::diff::FileStats> for FileStats {
+
    fn from(value: surf::diff::FileStats) -> Self {
+
        Self {
+
            additions: value.additions,
+
            deletions: value.deletions,
+
        }
+
    }
+
}
+

+
#[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>,
+
}
modified crates/radicle-types/src/lib.rs
@@ -1,5 +1,7 @@
pub mod cobs;
pub mod config;
+
pub mod diff;
pub mod error;
pub mod repo;
+
pub mod syntax;
pub mod traits;
added crates/radicle-types/src/syntax.rs
@@ -0,0 +1,825 @@
+
use std::collections::HashMap;
+
use std::fs;
+
use std::path::{Path, PathBuf};
+

+
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;
+

+
/// Highlight groups enabled.
+
const HIGHLIGHTS: &[&str] = &[
+
    "attribute",
+
    "constant",
+
    "constant.builtin",
+
    "comment",
+
    "constructor",
+
    "declare",
+
    "export",
+
    "function.builtin",
+
    "function",
+
    "identifier",
+
    "integer_literal",
+
    "float.literal",
+
    "keyword",
+
    "label",
+
    "module",
+
    "number",
+
    "operator",
+
    "property",
+
    "punctuation",
+
    "punctuation.bracket",
+
    "punctuation.delimiter",
+
    "punctuation.special",
+
    "shorthand_property_identifier",
+
    "string",
+
    "string.special",
+
    "statement",
+
    "tag",
+
    "type",
+
    "type.builtin",
+
    "type_annotation",
+
    "variable",
+
    "variable.builtin",
+
    "variable.parameter",
+
    "text",
+
    "text.literal",
+
    "text.title",
+
];
+

+
/// 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`.
+
#[derive(Default)]
+
pub struct Highlighter {
+
    configs: HashMap<&'static str, ts::HighlightConfiguration>,
+
}
+

+
impl Highlighter {
+
    /// Highlight a source code file.
+
    pub fn highlight(&mut self, path: &Path, code: &[u8]) -> Result<Vec<Line>, ts::Error> {
+
        let mut highlighter = ts::Highlighter::new();
+
        let Some(config) = 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());
+
        };
+
        config.configure(HIGHLIGHTS);
+

+
        let highlights = highlighter.highlight(config, code, None, |_| {
+
            // Language injection callback.
+
            None
+
        })?;
+

+
        Builder::default().run(highlights, code)
+
    }
+

+
    /// Detect language.
+
    fn detect(&mut self, path: &Path, _code: &[u8]) -> Option<&mut ts::HighlightConfiguration> {
+
        match path.extension().and_then(|e| e.to_str()) {
+
            Some("rs") => self.config("rust"),
+
            Some("svelte") => self.config("svelte"),
+
            Some("ts" | "js") => self.config("typescript"),
+
            Some("json") => self.config("json"),
+
            Some("sh" | "bash") => self.config("shell"),
+
            Some("md" | "markdown") => self.config("markdown"),
+
            Some("go") => self.config("go"),
+
            Some("c") => self.config("c"),
+
            Some("py") => self.config("python"),
+
            Some("rb") => self.config("ruby"),
+
            Some("tsx") => self.config("tsx"),
+
            Some("html") | Some("htm") | Some("xml") => self.config("html"),
+
            Some("css") => self.config("css"),
+
            Some("toml") => self.config("toml"),
+
            _ => None,
+
        }
+
    }
+

+
    /// Get a language configuration.
+
    fn config(&mut self, language: &'static str) -> Option<&mut ts::HighlightConfiguration> {
+
        match language {
+
            "rust" => Some(self.configs.entry(language).or_insert_with(|| {
+
                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")
+
            })),
+
            "json" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_json::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_json::HIGHLIGHTS_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "typescript" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+
                    language,
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_javascript::HIGHLIGHT_QUERY,
+
                        tree_sitter_typescript::HIGHLIGHTS_QUERY
+
                    ),
+
                    tree_sitter_javascript::INJECTIONS_QUERY,
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_javascript::LOCALS_QUERY,
+
                        tree_sitter_typescript::LOCALS_QUERY
+
                    ),
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "markdown" => Some(self.configs.entry(language).or_insert_with(|| {
+
                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(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_css::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_css::HIGHLIGHTS_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "go" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_go::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_go::HIGHLIGHTS_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "shell" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_bash::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_bash::HIGHLIGHT_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "c" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_c::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_c::HIGHLIGHT_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "python" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_python::LANGUAGE.into(),
+
                    language,
+
                    tree_sitter_python::HIGHLIGHTS_QUERY,
+
                    "",
+
                    "",
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "svelte" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_svelte_ng::LANGUAGE.into(),
+
                    language,
+
                    &format!(
+
                        "{}\n{}\n{}",
+
                        tree_sitter_javascript::HIGHLIGHT_QUERY,
+
                        tree_sitter_typescript::HIGHLIGHTS_QUERY,
+
                        tree_sitter_svelte_ng::HIGHLIGHTS_QUERY
+
                    ),
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_javascript::INJECTIONS_QUERY,
+
                        tree_sitter_svelte_ng::INJECTIONS_QUERY,
+
                    ),
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_typescript::LOCALS_QUERY,
+
                        tree_sitter_svelte_ng::LOCALS_QUERY,
+
                    ),
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "ruby" => Some(self.configs.entry(language).or_insert_with(|| {
+
                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")
+
            })),
+
            "tsx" => Some(self.configs.entry(language).or_insert_with(|| {
+
                ts::HighlightConfiguration::new(
+
                    tree_sitter_typescript::LANGUAGE_TSX.into(),
+
                    language,
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_javascript::HIGHLIGHT_QUERY,
+
                        tree_sitter_typescript::HIGHLIGHTS_QUERY
+
                    ),
+
                    tree_sitter_javascript::INJECTIONS_QUERY,
+
                    &format!(
+
                        "{}\n{}",
+
                        tree_sitter_javascript::LOCALS_QUERY,
+
                        tree_sitter_typescript::LOCALS_QUERY
+
                    ),
+
                )
+
                .expect("Highlighter::config: highlight configuration must be valid")
+
            })),
+
            "html" => Some(self.configs.entry(language).or_insert_with(|| {
+
                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(self.configs.entry(language).or_insert_with(|| {
+
                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)?;
+

+
        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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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: &mut 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,
+
                    }
+
                }
+
            }
+
        }
+
    }
+
}
modified crates/radicle-types/src/traits/repo.rs
@@ -11,8 +11,10 @@ use radicle::storage::{ReadRepository, ReadStorage, RepositoryInfo};
use radicle::{git, identity};

use crate::cobs;
+
use crate::diff::Diff;
use crate::error::Error;
use crate::repo;
+
use crate::syntax::{Highlighter, ToPretty};
use crate::traits::Profile;

#[derive(Serialize, Deserialize, PartialEq)]
@@ -130,20 +132,16 @@ pub trait Repo: Profile {
        })
    }

-
    fn get_diff(
-
        &self,
-
        rid: identity::RepoId,
-
        options: cobs::diff::Options,
-
    ) -> Result<surf::diff::Diff, Error> {
+
    fn get_diff(&self, rid: identity::RepoId, options: cobs::diff::Options) -> Result<Diff, Error> {
+
        let unified = options.unified.unwrap_or(5);
+
        let highlight = options.highlight.unwrap_or(true);
        let profile = self.profile();
        let repo = profile.storage.repository(rid)?.backend;
        let base = repo.find_commit(*options.base)?;
        let head = repo.find_commit(*options.head)?;

        let mut opts = git::raw::DiffOptions::new();
-
        opts.patience(true)
-
            .minimal(true)
-
            .context_lines(options.unified);
+
        opts.patience(true).minimal(true).context_lines(unified);

        let mut find_opts = git::raw::DiffFindOptions::new();
        find_opts.exact_match_only(true);
@@ -156,6 +154,12 @@ pub trait Repo: Profile {
        diff.find_similar(Some(&mut find_opts))?;
        let diff = surf::diff::Diff::try_from(diff)?;

-
        Ok::<_, Error>(diff)
+
        if highlight {
+
            let mut hi = Highlighter::default();
+

+
            return Ok::<_, Error>(diff.pretty(&mut hi, &(), &repo));
+
        }
+

+
        Ok::<_, Error>(diff.into())
    }
}
modified index.html
@@ -64,6 +64,7 @@
    <link rel="stylesheet" type="text/css" href="/typography.css" />
    <link rel="stylesheet" type="text/css" href="/prettylights.css" />
    <link rel="stylesheet" type="text/css" href="/colors.css" />
+
    <link rel="stylesheet" type="text/css" href="/syntax.css" />
    <script type="module">
      // Make global 'Buffer' available to legacy modules.
      import { Buffer } from "buffer";
modified package.json
@@ -16,7 +16,7 @@
    "check-rs": "scripts/check-rs",
    "test:e2e": "TZ='UTC' playwright test",
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
-
    "generate-types": "cargo test --manifest-path ./crates/radicle-types/Cargo.toml && npx prettier ./crates/radicle-types/bindings --write",
+
    "generate-types": "cargo test --manifest-path ./crates/radicle-types/Cargo.toml",
    "tauri": "npx tauri"
  },
  "engines": {
added public/syntax.css
@@ -0,0 +1,60 @@
+
.syntax.operator,
+
.syntax.keyword {
+
  color: var(--color-prettylights-syntax-keyword);
+
}
+
.syntax.type {
+
  color: var(--color-prettylights-syntax-entity);
+
}
+
.syntax.property,
+
.syntax.variable\.parameter {
+
  color: var(--color-prettylights-syntax-variable);
+
}
+
.syntax.punctuation\.delimiter {
+
}
+
.syntax.punctuation\.bracket {
+
}
+
.syntax.attribute {
+
}
+
.syntax.number,
+
.syntax.constant,
+
.syntax.type\.builtin,
+
.syntax.constant\.builtin,
+
.syntax.variable\.builtin,
+
.syntax.function {
+
  color: var(--color-prettylights-syntax-constant);
+
}
+
.syntax.comment {
+
  color: var(--color-prettylights-syntax-comment);
+
}
+
.syntax.string {
+
  color: var(--color-prettylights-syntax-string);
+
}
+
.syntax.string.special {
+
}
+
.syntax.function {
+
}
+
.syntax.type.builtin {
+
}
+
.syntax.punctuation.bracket {
+
}
+
.syntax.punctuation.delimiter {
+
}
+
.syntax.punctuation.special {
+
}
+
.syntax.text.literal {
+
}
+
.syntax.text.title {
+
}
+
.syntax.variable {
+
  color: var(--color-prettylights-syntax-storage-modifier-import);
+
}
+
.syntax.attribute {
+
}
+
.syntax.label {
+
}
+
.syntax.type {
+
}
+
.syntax.variable.parameter {
+
}
+
.syntax.constructor {
+
}
modified scripts/check-js
@@ -4,4 +4,4 @@ set -e
npx tsc --noEmit
npx svelte-check --tsconfig tsconfig.json --fail-on-warnings --compiler-warnings missing-custom-element-compile-options:ignore
npx eslint --cache --cache-location node_modules/.cache/eslint --max-warnings 0 .
-
npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" --ignore-path .gitignore --check --cache
+
npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" !"crates/**/*" --ignore-path .gitignore --check --cache
added src/components/Changeset.svelte
@@ -0,0 +1,29 @@
+
<script lang="ts">
+
  import type { Diff } from "@bindings/diff/Diff";
+

+
  import FileDiff from "./Changeset/FileDiff.svelte";
+

+
  interface Props {
+
    diff: Diff;
+
    repoId: string;
+
  }
+

+
  const { diff }: Props = $props();
+
</script>
+

+
<style>
+
  .diff-list {
+
    display: flex;
+
    flex-direction: column;
+
  }
+
</style>
+

+
<div class="diff-list">
+
  {#each diff.files as file}
+
    <FileDiff
+
      filePath={"path" in file ? file.path : file.newPath}
+
      oldFilePath={"oldPath" in file ? file.oldPath : undefined}
+
      fileDiff={file.diff}
+
      headerBadgeCaption={file.status} />
+
  {/each}
+
</div>
added src/components/Changeset/FileDiff.svelte
@@ -0,0 +1,290 @@
+
<script lang="ts">
+
  import type { DiffContent } from "@bindings/diff/DiffContent";
+
  import type { FileDiff } from "@bindings/diff/FileDiff";
+
  import type { Modification } from "@bindings/diff/Modification";
+

+
  import File from "@app/components/File.svelte";
+

+
  interface Props {
+
    filePath: string;
+
    oldFilePath?: string | undefined;
+
    fileDiff: DiffContent;
+
    headerBadgeCaption: FileDiff["status"];
+
  }
+

+
  const {
+
    filePath,
+
    oldFilePath = undefined,
+
    fileDiff,
+
    headerBadgeCaption,
+
  }: Props = $props();
+

+
  $inspect(headerBadgeCaption);
+

+
  function lineNumberR(line: Modification): string | number {
+
    switch (line.type) {
+
      case "addition": {
+
        return line.lineNo;
+
      }
+
      case "context": {
+
        return line.lineNoNew;
+
      }
+
      case "deletion": {
+
        return " ";
+
      }
+
    }
+
  }
+

+
  function lineNumberL(line: Modification): string | number {
+
    switch (line.type) {
+
      case "addition": {
+
        return " ";
+
      }
+
      case "context": {
+
        return line.lineNoOld;
+
      }
+
      case "deletion": {
+
        return line.lineNo;
+
      }
+
    }
+
  }
+

+
  function lineSign(line: Modification): string {
+
    switch (line.type) {
+
      case "addition": {
+
        return "+";
+
      }
+
      case "context": {
+
        return " ";
+
      }
+
      case "deletion": {
+
        return "-";
+
      }
+
    }
+
  }
+
</script>
+

+
<style>
+
  .container {
+
    font-size: var(--font-size-small);
+
    background: var(--color-background-float);
+
    border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
+
    overflow-x: auto;
+
  }
+
  .actions {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    gap: 1rem;
+
  }
+
  .browse {
+
    margin-left: auto;
+
  }
+
  .expand-button {
+
    cursor: pointer;
+
    user-select: none;
+
    margin-right: 0.5rem;
+
  }
+
  .diff {
+
    font-family: var(--font-family-monospace);
+
    table-layout: fixed;
+
    border-collapse: collapse;
+
    margin: 0.5rem 0;
+
  }
+
  .diff-line {
+
    vertical-align: top;
+
  }
+
  .diff-line.type-addition > * {
+
    background-color: var(--color-fill-diff-green-light);
+
  }
+
  .diff-line.type-deletion > * {
+
    background-color: var(--color-fill-diff-red-light);
+
  }
+

+
  .diff-line.selected > * {
+
    background-color: var(--color-fill-float-hover);
+
  }
+
  .diff-line.selected.type-addition > * {
+
    background-color: var(--color-fill-diff-green);
+
  }
+
  .diff-line.selected.type-deletion > * {
+
    background-color: var(--color-fill-diff-red);
+
  }
+

+
  .type-addition > .diff-line-number,
+
  .type-addition > .diff-line-type {
+
    color: var(--color-foreground-success);
+
  }
+
  .type-deletion > .diff-line-number,
+
  .type-deletion > .diff-line-type {
+
    color: var(--color-foreground-red);
+
  }
+

+
  .diff-line.selected .selection-indicator-left {
+
    background-color: var(--color-fill-secondary);
+
  }
+
  .type-addition.diff-line.selected .selection-indicator-left {
+
    background-color: var(--color-fill-secondary);
+
  }
+
  .type-deletion.diff-line.selected .selection-indicator-left {
+
    background-color: var(--color-fill-secondary);
+
  }
+

+
  .diff-line.selected .selection-indicator-right {
+
    background-color: var(--color-fill-secondary);
+
  }
+
  .type-addition.diff-line.selected .selection-indicator-right {
+
    background-color: var(--color-fill-secondary);
+
  }
+
  .type-deletion.diff-line.selected .selection-indicator-right {
+
    background-color: var(--color-fill-secondary);
+
  }
+

+
  .selection-start {
+
    box-shadow: 0 -1px 0 0 var(--color-fill-secondary);
+
    z-index: 1;
+
  }
+
  .selection-end {
+
    box-shadow: 0 1px 0 0 var(--color-fill-secondary);
+
    z-index: 1;
+
  }
+

+
  .selection-start.selection-end {
+
    box-shadow: 0 0 0 1px var(--color-fill-secondary);
+
    z-index: 1;
+
  }
+

+
  .diff-line-number {
+
    font-family: var(--font-family-monospace);
+
    text-align: right;
+
    user-select: none;
+
    line-height: 1.5rem;
+
    min-width: 3rem;
+
    cursor: pointer;
+
    color: var(--color-foreground-disabled);
+
  }
+
  .diff-line-number.left {
+
    position: relative;
+
    padding: 0 0.5rem 0 0.75rem;
+
  }
+
  .selection-indicator-left {
+
    position: absolute;
+
    left: 0;
+
    top: 0;
+
    bottom: 0;
+
    width: 1px;
+
  }
+
  .selection-indicator-right {
+
    position: absolute;
+
    right: 0;
+
    top: 0;
+
    bottom: 0;
+
    width: 1px;
+
  }
+
  .diff-line-number.right {
+
    padding: 0 0.75rem 0 0.5rem;
+
  }
+
  .diff-line-content {
+
    color: unset !important;
+
    white-space: pre-wrap;
+
    overflow-wrap: anywhere;
+
    width: 100%;
+
    padding-right: 0.5rem;
+
  }
+
  .diff-line-type {
+
    text-align: center;
+
    padding-left: 0.75rem;
+
    padding-right: 0.75rem;
+
    user-select: none;
+
  }
+
  .diff-expand-header {
+
    padding-left: 0.5rem;
+
    color: var(--color-foreground-dim);
+
  }
+
</style>
+

+
<File>
+
  {#snippet leftHeader()}
+
    {#if (headerBadgeCaption === "moved" || headerBadgeCaption === "copied") && oldFilePath}
+
      <span style="display: flex; align-items: center; flex-wrap: wrap;">
+
        {oldFilePath}
+
        <span style:padding="0 0.5rem">→</span>
+
        {filePath}
+
      </span>
+
    {:else}
+
      {filePath}
+
    {/if}
+

+
    {#if headerBadgeCaption === "added"}
+
      added
+
    {:else if headerBadgeCaption === "deleted"}
+
      deleted
+
    {:else if headerBadgeCaption === "moved"}
+
      moved
+
    {:else if headerBadgeCaption === "copied"}
+
      copied
+
    {/if}
+
  {/snippet}
+

+
  {#snippet children()}
+
    <div class="container">
+
      {#if fileDiff.type === "plain"}
+
        {#if fileDiff.hunks.length > 0}
+
          <table class="diff" data-file-diff-select>
+
            {#each fileDiff.hunks as hunk, hunkIdx}
+
              <!-- svelte-ignore node_invalid_placement_ssr -->
+
              <tr class="diff-line hunk-header">
+
                <td colspan={2} style:position="relative">
+
                  <div class="selection-indicator-left"></div>
+
                </td>
+
                <td
+
                  colspan={6}
+
                  class="diff-expand-header"
+
                  style:position="relative">
+
                  {hunk.header}
+
                  <div class="selection-indicator-right"></div>
+
                </td>
+
              </tr>
+
              {#each hunk.lines as line, lineIdx}
+
                <!-- svelte-ignore node_invalid_placement_ssr -->
+
                <tr
+
                  style="position: relative;"
+
                  class={`diff-line type-${line.type}`}>
+
                  <td
+
                    id={[filePath, "H" + hunkIdx, "L" + lineIdx].join("-")}
+
                    class="diff-line-number left">
+
                    <div class="selection-indicator-left"></div>
+
                    {lineNumberL(line)}
+
                  </td>
+
                  <td class="diff-line-number right">
+
                    {lineNumberR(line)}
+
                  </td>
+
                  <td class="diff-line-type" data-line-type={line.type}>
+
                    {lineSign(line)}
+
                  </td>
+
                  <td class="diff-line-content">
+
                    {#if line.highlight}
+
                      {@html line.highlight.items
+
                        .map(
+
                          s =>
+
                            `<span class="syntax ${s.style}">${s.item}</span>`,
+
                        )
+
                        .join("")}
+
                    {:else}
+
                      {line.line}
+
                    {/if}
+
                  </td>
+
                  <td class="selection-indicator-right"></td>
+
                </tr>
+
              {/each}
+
            {/each}
+
          </table>
+
        {:else}
+
          <div style:margin="1rem 0">Empty file</div>
+
        {/if}
+
      {:else}
+
        <div style:margin="1rem 0">Empty file</div>
+
      {/if}
+
    </div>
+
  {/snippet}
+
</File>
added src/components/File.svelte
@@ -0,0 +1,70 @@
+
<script lang="ts">
+
  import type { Snippet } from "svelte";
+

+
  interface Props {
+
    sticky?: boolean;
+
    leftHeader: Snippet;
+
    children: Snippet;
+
  }
+

+
  const { sticky = true, leftHeader, children }: Props = $props();
+
</script>
+

+
<style>
+
  .header {
+
    display: flex;
+
    height: 3rem;
+
    align-items: center;
+
    padding: 0 0.5rem 0 1rem;
+
    border: 1px solid var(--color-border-hint);
+
    border-top-left-radius: var(--border-radius-small);
+
    border-top-right-radius: var(--border-radius-small);
+
    background-color: var(--color-background-default);
+
    z-index: 2;
+
  }
+

+
  .sticky {
+
    position: sticky;
+
    top: 0;
+
  }
+

+
  .left {
+
    display: flex;
+
    gap: 0.5rem;
+
    margin-right: 1rem;
+
    align-items: center;
+
  }
+

+
  .container {
+
    position: relative;
+
    overflow-x: auto;
+
    border: 1px solid var(--color-border-hint);
+
    border-top: 0;
+
    background: var(--color-background-float);
+
    border-bottom-left-radius: var(--border-radius-small);
+
    border-bottom-right-radius: var(--border-radius-small);
+
  }
+
  @media (max-width: 719.98px) {
+
    .header {
+
      border-radius: 0;
+
      border-left: 0;
+
      border-right: 0;
+
      padding: 0 1rem 0 1rem;
+
    }
+
    .container {
+
      border-radius: 0;
+
      border-left: 0;
+
      border-right: 0;
+
    }
+
  }
+
</style>
+

+
<div class="header" class:sticky>
+
  <div class="left">
+
    {@render leftHeader()}
+
  </div>
+
</div>
+

+
<div class="container">
+
  {@render children()}
+
</div>
modified src/views/repo/Patch.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+
  import type { Diff } from "@bindings/diff/Diff";
  import type { Config } from "@bindings/config/Config";
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
  import type { Patch } from "@bindings/cob/patch/Patch";
@@ -17,6 +18,7 @@
  import NodeId from "@app/components/NodeId.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
+
  import Changeset from "@app/components/Changeset.svelte";

  interface Props {
    repo: RepoInfo;
@@ -40,6 +42,18 @@
    more = patches.more;
  });

+
  async function loadHighlightedDiff(rid: string, base: string, head: string) {
+
    return invoke<Diff>("get_diff", {
+
      rid,
+
      options: {
+
        base,
+
        head,
+
        unified: 5,
+
        highlight: true,
+
      },
+
    });
+
  }
+

  async function loadPatch(rid: string, patchId: string) {
    patch = await invoke<Patch>("patch_by_id", {
      rid: rid,
@@ -177,6 +191,9 @@
    <div class="txt-small" style:margin-top="1rem">Revisions</div>
    {#each revisions as revision}
      <div><Id id={revision.id} variant="oid" /></div>
+
      {#await loadHighlightedDiff(repo.rid, revision.base, revision.head) then diff}
+
        <Changeset {diff} repoId={repo.rid} />
+
      {/await}
    {/each}
  </div>
</Layout>