1
0
Fork 0
mirror of https://github.com/helix-editor/helix synced 2024-05-07 23:46:05 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Poliorcetics 3819440345
Merge 0ea496e36c into 5ee7411450 2024-04-26 22:50:33 +01:00
Diogenesoftoronto 5ee7411450
Change cursor color per mode for default (#10608) 2024-04-26 16:50:29 -05:00
Keir Lawson 31248d4e2f
Enable metals inlay hints (#10597) 2024-04-26 16:48:23 -05:00
Alexis (Poliorcetics) Bourget 0ea496e36c
feat: Add Jujutsu diff handling to Helix 2024-04-10 20:07:19 +02:00
Alexis (Poliorcetics) Bourget 7a3bd180d5
chore: don't conflate Dummy and Git providers 2024-04-10 20:07:19 +02:00
9 changed files with 262 additions and 17 deletions

View File

@ -13,10 +13,14 @@ repository.workspace = true
homepage.workspace = true
[features]
default = ["git"]
default = ["vcs"]
unicode-lines = ["helix-core/unicode-lines"]
integration = ["helix-event/integration_test"]
# All VCSes available for diffs in Helix
vcs = ["git", "jujutsu"]
git = ["helix-vcs/git"]
jujutsu = ["helix-vcs/jujutsu"]
[[bin]]
name = "hx"

View File

@ -24,9 +24,13 @@ imara-diff = "0.1.5"
anyhow = "1"
log = "0.4"
# For `jujutsu`
tempfile = { version = "3.10", optional = true }
[features]
git = ["gix"]
jujutsu = ["tempfile"]
[dev-dependencies]
tempfile = "3.10"

View File

@ -17,7 +17,7 @@
};
use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
use crate::{DiffProvider, FileChange};
use crate::FileChange;
#[cfg(test)]
mod test;
@ -198,12 +198,6 @@ pub fn for_each_changed_file(
}
}
impl From<Git> for DiffProvider {
fn from(value: Git) -> Self {
DiffProvider::Git(value)
}
}
/// Finds the object that contains the contents of a file at a specific commit.
fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Result<ObjectId> {
let repo_dir = repo.work_dir().context("repo has no worktree")?;

View File

@ -2,7 +2,7 @@
use tempfile::TempDir;
use crate::Git;
use crate::git::Git;
fn exec_git_cmd(args: &str, git_dir: &Path) {
let res = Command::new("git")

229
helix-vcs/src/jujutsu.rs Normal file
View File

@ -0,0 +1,229 @@
//! Jujutsu works with several backends and could add new ones in the future. Private builds of
//! it could also have private backends. Those make it hard to use `jj-lib` since it won't have
//! access to newer or private backends and fail to compute the diffs for them.
//!
//! Instead in case there *is* a diff to base ourselves on, we copy it to a tempfile or just use the
//! current file if not.
use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use anyhow::{Context, Ok, Result};
use arc_swap::ArcSwap;
use crate::FileChange;
#[derive(Clone)]
pub struct Jujutsu;
impl Jujutsu {
pub(crate) fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
let jj_root_dir = find_jj_root(file)?;
// We extracted the `jj_root_dir` from the file itself, if stripping the prefix fails
// something has gone very very wrong
let file_rel_to_dot_jj = file
.strip_prefix(jj_root_dir)
.expect("failed to strip diff path from jj root dir");
let tmpfile = tempfile::NamedTempFile::with_prefix("helix-jj-diff-")
.context("could not create tempfile to save jj diff base")?;
let tmppath = tmpfile.path();
let copy_bin = if cfg!(windows) { "copy.exe" } else { "cp" };
let status = Command::new("jj")
.arg("--repository")
.arg(jj_root_dir)
.args([
"--ignore-working-copy",
"diff",
"--revision",
"@",
"--config-toml",
])
// Copy the temporary file provided by jujutsu to a temporary path of our own,
// because the `$left` directory is deleted when `jj` finishes executing.
.arg(format!(
"ui.diff.tool = ['{exe}', '$left/{base}', '{target}']",
exe = copy_bin,
base = file_rel_to_dot_jj.display(),
// Where to copy the jujutsu-provided file
target = tmppath.display(),
))
// Restrict the diff to the current file
.arg(file)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.context("failed to execute jj diff command")?;
let use_jj_path =
status.success() && std::fs::metadata(tmppath).map_or(false, |m| m.len() > 0);
// If the copy call inside `jj diff` succeeded, the tempfile is the one containing the base
// else it's just the original file (so no diff). We check for size since `jj` can return
// 0-sized files when there are no diffs to present for the file.
let diff_base_path = if use_jj_path { tmppath } else { file };
// If the command succeeded, it means we either copied the jujutsu base or the current file,
// so there should always be something to read and compare to.
std::fs::read(diff_base_path).context("could not read jj diff base from the target")
}
pub(crate) fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
let jj_root_dir = find_jj_root(file)?;
// See <https://github.com/martinvonz/jj/blob/main/docs/templates.md>
//
// This will produce the following:
//
// - If there are no branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm`
// - If there is a single branch: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master)`
// - If there are 2+ branches: `vyvqwlmsvnlkmqrvqktpuluvuknuxpmm (master, jj-diffs)`
//
// Always using the long id makes it easy to share it with others, which would not be the
// case for shorter ones: they could have a local change that renders it ambiguous.
let template = r#"separate(" ", change_id, surround("(", ")", branches.join(", ")))"#;
let out = Command::new("jj")
.arg("--repository")
.arg(jj_root_dir)
.args([
"--ignore-working-copy",
"log",
"--color",
"never",
"--revisions",
"@", // Only display the current revision
"--no-graph",
"--no-pager",
"--template",
template,
])
.output()?;
if !out.status.success() {
anyhow::bail!("jj log command executed but failed");
}
let out = String::from_utf8(out.stdout)?;
let rev = out
.lines()
.next()
.context("should always find at least one line")?;
Ok(Arc::new(ArcSwap::from_pointee(rev.into())))
}
pub(crate) fn for_each_changed_file(
&self,
cwd: &Path,
callback: impl Fn(Result<FileChange>) -> bool,
) -> Result<()> {
let jj_root_dir = find_jj_root(cwd)?;
let out = Command::new("jj")
.arg("--repository")
.arg(jj_root_dir)
.args([
"--ignore-working-copy",
"log",
"--color",
"never",
"--revisions",
"@", // Only display the current revision
"--no-graph",
"--no-pager",
"--template",
"",
"--types",
])
.arg(cwd)
.output()?;
if !out.status.success() {
anyhow::bail!("jj log command executed but failed");
}
let out = String::from_utf8(out.stdout)?;
for line in out.lines() {
let mut split = line.splitn(2, ' ');
let Some(status) = split.next() else { continue; };
let Some(path) = split.next() else { continue; };
let Some(change) = status_to_change(status, path) else { continue };
if !callback(Ok(change)) {
break;
}
}
Ok(())
}
}
/// Move up until we find the repository's root
fn find_jj_root(file: &Path) -> Result<&Path> {
file.ancestors()
.find(|p| p.join(".jj").exists())
.context("no .jj dir found in parents")
}
/// Associate a status to a `FileChange`.
fn status_to_change(status: &str, path: &str) -> Option<FileChange> {
// Syntax: <https://github.com/martinvonz/jj/blob/320f50e00fcbd0d3ce27feb1e14b8e36d76b658f/cli/src/diff_util.rs#L68>
Some(match status {
"FF" | "LL" | "CF" | "CL" | "FL" | "LF" => FileChange::Modified { path: path.into() },
"-F" | "-L" => FileChange::Untracked { path: path.into() },
"F-" | "L-" => FileChange::Deleted { path: path.into() },
"FC" | "LC" => FileChange::Conflict { path: path.into() },
// We ignore gitsubmodules here since they not interesting in the context of
// a file editor.
_ => return None,
})
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn test_status_to_change() {
let p = "helix-vcs/src/lib.rs";
let pb = PathBuf::from(p);
for s in ["FF", "LL", "CF", "CL", "FL", "LF"] {
assert_eq!(
status_to_change(s, p).unwrap(),
FileChange::Modified { path: pb.clone() }
);
}
for s in ["-F", "-L"] {
assert_eq!(
status_to_change(s, p).unwrap(),
FileChange::Untracked { path: pb.clone() }
);
}
for s in ["F-", "L-"] {
assert_eq!(
status_to_change(s, p).unwrap(),
FileChange::Deleted { path: pb.clone() }
);
}
for s in ["FC", "LC"] {
assert_eq!(
status_to_change(s, p).unwrap(),
FileChange::Conflict { path: pb.clone() }
);
}
for s in ["GG", "LG", "ARO", "", " ", " "] {
assert_eq!(status_to_change(s, p), None);
}
}
}

View File

@ -5,14 +5,12 @@
sync::Arc,
};
#[cfg(feature = "git")]
pub use git::Git;
#[cfg(not(feature = "git"))]
pub use Dummy as Git;
#[cfg(feature = "git")]
mod git;
#[cfg(feature = "jujutsu")]
mod jujutsu;
mod diff;
pub use diff::{DiffHandle, Hunk};
@ -104,7 +102,12 @@ impl Default for DiffProviderRegistry {
fn default() -> Self {
// currently only git is supported
// TODO make this configurable when more providers are added
let providers = vec![Git.into()];
let providers = vec![
#[cfg(feature = "git")]
DiffProvider::Git(git::Git),
#[cfg(feature = "jujutsu")]
DiffProvider::Jujutsu(jujutsu::Jujutsu),
];
DiffProviderRegistry { providers }
}
}
@ -115,7 +118,9 @@ fn default() -> Self {
pub enum DiffProvider {
Dummy(Dummy),
#[cfg(feature = "git")]
Git(Git),
Git(git::Git),
#[cfg(feature = "jujutsu")]
Jujutsu(jujutsu::Jujutsu),
}
impl DiffProvider {
@ -124,6 +129,8 @@ fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
Self::Dummy(inner) => inner.get_diff_base(file),
#[cfg(feature = "git")]
Self::Git(inner) => inner.get_diff_base(file),
#[cfg(feature = "jujutsu")]
Self::Jujutsu(inner) => inner.get_diff_base(file),
}
}
@ -132,6 +139,8 @@ fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
Self::Dummy(inner) => inner.get_current_head_name(file),
#[cfg(feature = "git")]
Self::Git(inner) => inner.get_current_head_name(file),
#[cfg(feature = "jujutsu")]
Self::Jujutsu(inner) => inner.get_current_head_name(file),
}
}
@ -144,6 +153,8 @@ fn for_each_changed_file(
Self::Dummy(inner) => inner.for_each_changed_file(cwd, f),
#[cfg(feature = "git")]
Self::Git(inner) => inner.for_each_changed_file(cwd, f),
#[cfg(feature = "jujutsu")]
Self::Jujutsu(inner) => inner.for_each_changed_file(cwd, f),
}
}
}

View File

@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq)]
pub enum FileChange {
Untracked {
path: PathBuf,

View File

@ -53,7 +53,7 @@ ltex-ls = { command = "ltex-ls" }
markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
markdown-oxide = { command = "markdown-oxide" }
marksman = { command = "marksman", args = ["server"] }
metals = { command = "metals", config = { "isHttpEnabled" = true } }
metals = { command = "metals", config = { "isHttpEnabled" = true, metals = { inlayHints = { typeParameters = {enable = true} , hintsInPatternMatch = {enable = true} } } } }
mint = { command = "mint", args = ["ls"] }
nil = { command = "nil" }
nimlangserver = { command = "nimlangserver" }

View File

@ -66,6 +66,8 @@ label = "honey"
# TODO: namespace ui.cursor as ui.selection.cursor?
"ui.cursor.select" = { bg = "delta" }
"ui.cursor.insert" = { bg = "white" }
"ui.cursor.primary.select" = { bg = "delta" }
"ui.cursor.primary.insert" = { bg = "white" }
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "bossanova" }