From a870a1625c074fe1053bc0ba69e2a943f94ab41f Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 6 Feb 2022 16:01:53 +0530 Subject: [PATCH 1/3] Show git diff signs in gutter --- Cargo.lock | 112 ++++++++++++++++++++++ Cargo.toml | 1 + helix-vcs/Cargo.toml | 18 ++++ helix-vcs/src/git.rs | 189 +++++++++++++++++++++++++++++++++++++ helix-vcs/src/lib.rs | 48 ++++++++++ helix-view/Cargo.toml | 1 + helix-view/src/document.rs | 28 +++++- helix-view/src/editor.rs | 8 +- helix-view/src/gutter.rs | 27 ++++++ helix-view/src/view.rs | 1 + 10 files changed, 429 insertions(+), 4 deletions(-) create mode 100644 helix-vcs/Cargo.toml create mode 100644 helix-vcs/src/git.rs create mode 100644 helix-vcs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 85438e01ec12..b5ae4dfadda6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,9 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -221,6 +224,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "fern" version = "0.6.0" @@ -302,6 +314,19 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "git2" +version = "0.13.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "globset" version = "0.4.8" @@ -479,6 +504,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "helix-vcs" +version = "0.6.0" +dependencies = [ + "git2", + "similar", + "tempfile", +] + [[package]] name = "helix-view" version = "0.6.0" @@ -494,6 +528,7 @@ dependencies = [ "helix-dap", "helix-lsp", "helix-tui", + "helix-vcs", "log", "once_cell", "serde", @@ -544,12 +579,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "jsonrpc-core" version = "18.0.0" @@ -575,6 +628,18 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +[[package]] +name = "libgit2-sys" +version = "0.12.26+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.3" @@ -585,6 +650,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.6" @@ -758,6 +835,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "proc-macro2" version = "1.0.36" @@ -856,6 +939,15 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "retain_mut" version = "0.1.7" @@ -1054,6 +1146,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -1229,6 +1335,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 780811f7802c..1d86f7367106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "helix-lsp", "helix-dap", "helix-loader", + "helix-vcs", "xtask", ] diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml new file mode 100644 index 000000000000..8ea36b40297e --- /dev/null +++ b/helix-vcs/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "helix-vcs" +version = "0.6.0" +authors = ["Blaž Hrastnik "] +edition = "2021" +license = "MPL-2.0" +categories = ["editor"] +repository = "https://github.com/helix-editor/helix" +homepage = "https://helix-editor.com" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +git2 = { version = "0.13", default-features = false } +similar = "2.1" + +[dev-dependencies] +tempfile = "3.3" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs new file mode 100644 index 000000000000..748dee40c74a --- /dev/null +++ b/helix-vcs/src/git.rs @@ -0,0 +1,189 @@ +use std::{ + collections::HashMap, + ops::Range, + path::{Path, PathBuf}, +}; + +use git2::{Oid, Repository}; +use similar::DiffTag; + +use crate::{LineDiff, LineDiffs, RepoRoot}; + +pub struct Git { + repo: Repository, + /// Absolute path to root of the repo + root: RepoRoot, + head: Oid, + + /// A cache mapping absolute file paths to file contents + /// in the HEAD commit. + head_cache: HashMap, +} + +impl std::fmt::Debug for Git { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Git").field("root", &self.root).finish() + } +} + +impl Git { + pub fn head_commit_id(repo: &Repository) -> Option { + repo.head() + .and_then(|gitref| gitref.peel_to_commit()) + .map(|commit| commit.id()) + .ok() + } + + pub fn discover_from_path(file: &Path) -> Option { + let repo = Repository::discover(file).ok()?; + let root = repo.workdir()?.to_path_buf(); + let head_oid = Self::head_commit_id(&repo)?; + Some(Self { + repo, + root, + head: head_oid, + head_cache: HashMap::new(), + }) + } + + pub fn root(&self) -> &Path { + &self.root + } + + fn relative_to_root<'p>(&self, path: &'p Path) -> Option<&'p Path> { + path.strip_prefix(&self.root).ok() + } + + pub fn read_file_from_head(&mut self, file: &Path) -> Option<&str> { + let current_head = Self::head_commit_id(&self.repo)?; + // TODO: Check cache validity on events like WindowChange + // instead of on every keypress ? Will require hooks. + if current_head != self.head { + self.head_cache.clear(); + self.head = current_head; + } + + if !self.head_cache.contains_key(file) { + let relative = self.relative_to_root(file)?; + let revision = &format!("HEAD:{}", relative.display()); + let object = self.repo.revparse_single(revision).ok()?; + let blob = object.peel_to_blob().ok()?; + let contents = std::str::from_utf8(blob.content()).ok()?; + self.head_cache + .insert(file.to_path_buf(), contents.to_string()); + } + + self.head_cache.get(file).map(|s| s.as_str()) + } + + pub fn line_diff_with_head(&mut self, file: &Path, contents: &str) -> LineDiffs { + let base = match self.read_file_from_head(file) { + Some(b) => b, + None => return LineDiffs::new(), + }; + let mut config = similar::TextDiff::configure(); + config.timeout(std::time::Duration::from_millis(250)); + + let mut line_diffs: LineDiffs = HashMap::new(); + + let mut mark_lines = |range: Range, change: LineDiff| { + for line in range { + line_diffs.insert(line, change); + } + }; + + let diff = config.diff_lines(base, contents); + for op in diff.ops() { + let (tag, _, line_range) = op.as_tag_tuple(); + let start = line_range.start; + match tag { + DiffTag::Insert => mark_lines(line_range, LineDiff::Added), + DiffTag::Replace => mark_lines(line_range, LineDiff::Modified), + DiffTag::Delete => mark_lines(start..start + 1, LineDiff::Deleted), + DiffTag::Equal => (), + } + } + + line_diffs + } +} + +#[cfg(test)] +mod test { + use std::{ + fs::{self, File}, + process::Command, + }; + + use tempfile::TempDir; + + use super::*; + + fn empty_git_repo() -> TempDir { + let tmp = tempfile::tempdir().expect("Could not create temp dir for git testing"); + exec_git_cmd("init", tmp.path()); + tmp + } + + fn exec_git_cmd(args: &str, git_dir: &Path) { + Command::new("git") + .arg("-C") + .arg(git_dir) // execute the git command in this directory + .args(args.split_whitespace()) + .status() + .expect(&format!("`git {args}` failed")) + .success() + .then(|| ()) + .expect(&format!("`git {args}` failed")); + } + + #[test] + fn test_cannot_discover_bare_git_repo() { + let temp_git = empty_git_repo(); + let file = temp_git.path().join("file.txt"); + File::create(&file).expect("Could not create file"); + + assert!(Git::discover_from_path(&file).is_none()); + } + + #[test] + fn test_discover_git_repo() { + let temp_git = empty_git_repo(); + let file = temp_git.path().join("file.txt"); + File::create(&file).expect("Could not create file"); + exec_git_cmd("add file.txt", temp_git.path()); + exec_git_cmd("commit -m message", temp_git.path()); + + let root = Git::discover_from_path(&file).map(|g| g.root().to_owned()); + assert_eq!(Some(temp_git.path().to_owned()), root); + } + + #[test] + fn test_read_file_from_head() { + let tmp_repo = empty_git_repo(); + let git_dir = tmp_repo.path(); + let file = git_dir.join("file.txt"); + + let contents = r#" + a file with unnecessary + indent and text. + "#; + fs::write(&file, contents).expect("Could not write to file"); + exec_git_cmd("add file.txt", git_dir); + exec_git_cmd("commit -m message", git_dir); + + let mut git = Git::discover_from_path(&file).unwrap(); + assert_eq!( + Some(contents), + git.read_file_from_head(&file), + "Wrong blob contents from HEAD on clean index" + ); + + fs::write(&file, "new text").expect("Could not write to file"); + assert_eq!( + Some(contents), + git.read_file_from_head(&file), + "Wrong blob contents from HEAD when index is dirty" + ); + } +} diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs new file mode 100644 index 000000000000..a7abc9e39b15 --- /dev/null +++ b/helix-vcs/src/lib.rs @@ -0,0 +1,48 @@ +mod git; + +use std::{ + cell::RefCell, + collections::HashMap, + path::{Path, PathBuf}, + rc::Rc, +}; + +pub use git::Git; + +// TODO: Move to helix_core once we have a generic diff mode +#[derive(Copy, Clone, Debug)] +pub enum LineDiff { + Added, + Deleted, + Modified, +} + +/// Maps line numbers to changes +pub type LineDiffs = HashMap; + +pub type RepoRoot = PathBuf; + +#[derive(Debug, Default)] +pub struct Registry { + inner: HashMap>>, +} + +impl Registry { + pub fn new() -> Self { + Self::default() + } + + pub fn discover_from_path(&mut self, file: &Path) -> Option>> { + let cached_root = self.inner.keys().find(|root| file.starts_with(root)); + match cached_root { + Some(root) => self.inner.get(root).cloned(), + None => { + let repo = Git::discover_from_path(file)?; + let root = repo.root().to_path_buf(); + let repo = Rc::new(RefCell::new(repo)); + self.inner.insert(root, Rc::clone(&repo)); + Some(repo) + } + } + } +} diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index ce3f1af4b68f..72fd89ff5ded 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1" helix-core = { version = "0.6", path = "../helix-core" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } +helix-vcs = { version = "0.6", path = "../helix-vcs" } crossterm = { version = "0.23", optional = true } # Conversion traits diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index c9c1e502825c..7e39f029666c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,12 +1,14 @@ use anyhow::{anyhow, bail, Context, Error}; use helix_core::auto_pairs::AutoPairs; +use helix_vcs::LineDiffs; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; +use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; @@ -120,6 +122,8 @@ pub struct Document { diagnostics: Vec, language_server: Option>, + version_control: Option>>, + line_diffs: LineDiffs, } use std::{fmt, mem}; @@ -360,6 +364,8 @@ impl Document { last_saved_revision: 0, modified_since_accessed: false, language_server: None, + version_control: None, + line_diffs: LineDiffs::new(), } } @@ -612,6 +618,20 @@ impl Document { self.language_server = language_server; } + pub fn set_version_control(&mut self, vcs: Option>>) { + self.version_control = vcs; + } + + pub fn diff_with_vcs(&mut self) { + let vcs = self + .version_control + .as_ref() + .and_then(|v| v.try_borrow_mut().ok()); + if let Some((mut vcs, path)) = vcs.zip(self.path()) { + self.line_diffs = vcs.line_diff_with_head(path, &self.text().to_string()); + } + } + /// Select text within the [`Document`]. pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { // TODO: use a transaction? @@ -687,6 +707,8 @@ impl Document { tokio::spawn(notify); } } + + self.diff_with_vcs(); } success } @@ -858,6 +880,10 @@ impl Document { server.is_initialized().then(|| server) } + pub fn line_diffs(&self) -> &LineDiffs { + &self.line_diffs + } + #[inline] /// Tree-sitter AST tree pub fn syntax(&self) -> Option<&Syntax> { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9a2b4297f8b2..255a71952fec 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -309,6 +309,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub theme: Theme, pub language_servers: helix_lsp::Registry, + pub vcs_providers: helix_vcs::Registry, pub debugger: Option, pub debugger_events: SelectAll>, @@ -363,7 +364,6 @@ impl Editor { syn_loader: Arc, config: Box>, ) -> Self { - let language_servers = helix_lsp::Registry::new(); let conf = config.load(); let auto_pairs = (&conf.auto_pairs).into(); @@ -378,7 +378,8 @@ impl Editor { selected_register: None, macro_recording: None, theme: theme_loader.default(), - language_servers, + language_servers: helix_lsp::Registry::new(), + vcs_providers: helix_vcs::Registry::new(), debugger: None, debugger_events: SelectAll::new(), breakpoints: HashMap::new(), @@ -631,7 +632,8 @@ impl Editor { let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); - + doc.set_version_control(self.vcs_providers.discover_from_path(&path)); + doc.diff_with_vcs(); self.new_document(doc) }; diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 7327ed1a2030..dd052833545f 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,5 +1,7 @@ use std::fmt::Write; +use helix_vcs::LineDiff; + use crate::{ graphics::{Color, Modifier, Style}, Document, Editor, Theme, View, @@ -39,6 +41,31 @@ pub fn diagnostic<'doc>( }) } +pub fn git_diff<'doc>( + _editor: &'doc Editor, + doc: &'doc Document, + _view: &View, + theme: &Theme, + _is_focused: bool, + _width: usize, +) -> GutterFn<'doc> { + let added = theme.get("diff.plus"); + let deleted = theme.get("diff.minus"); + let modified = theme.get("diff.delta"); + + Box::new(move |line: usize, _selected: bool, out: &mut String| { + let diff = doc.line_diffs().get(&(line))?; + + let (icon, style) = match diff { + LineDiff::Added => ("▍", added), + LineDiff::Deleted => ("▔", deleted), + LineDiff::Modified => ("▍", modified), + }; + write!(out, "{}", icon).unwrap(); + Some(style) + }) +} + pub fn line_number<'doc>( editor: &'doc Editor, doc: &'doc Document, diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index c6ae0c56e851..0e407c6ad955 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -65,6 +65,7 @@ impl JumpList { } const GUTTERS: &[(Gutter, usize)] = &[ + (gutter::git_diff, 1), (gutter::diagnostics_or_breakpoints, 1), (gutter::line_number, 5), ]; From 2582167f9728d241bdb746b13e9498b00fc3f5b2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Thu, 10 Mar 2022 19:30:25 +0530 Subject: [PATCH 2/3] Avoid string allocation when git diffing --- Cargo.lock | 1 + helix-vcs/Cargo.toml | 1 + helix-vcs/src/git.rs | 38 +++++++++++++++++++++++--------------- helix-vcs/src/lib.rs | 1 + helix-vcs/src/rope.rs | 10 ++++++++++ helix-view/src/document.rs | 2 +- 6 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 helix-vcs/src/rope.rs diff --git a/Cargo.lock b/Cargo.lock index b5ae4dfadda6..87aa8259e989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,6 +509,7 @@ name = "helix-vcs" version = "0.6.0" dependencies = [ "git2", + "ropey", "similar", "tempfile", ] diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 8ea36b40297e..bb24d1f1e1c9 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://helix-editor.com" [dependencies] git2 = { version = "0.13", default-features = false } similar = "2.1" +ropey = "1.3" [dev-dependencies] tempfile = "3.3" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 748dee40c74a..8c9aa15ad2c2 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -2,12 +2,14 @@ use std::{ collections::HashMap, ops::Range, path::{Path, PathBuf}, + time::{Duration, Instant}, }; use git2::{Oid, Repository}; +use ropey::Rope; use similar::DiffTag; -use crate::{LineDiff, LineDiffs, RepoRoot}; +use crate::{rope::RopeLine, LineDiff, LineDiffs, RepoRoot}; pub struct Git { repo: Repository, @@ -17,7 +19,7 @@ pub struct Git { /// A cache mapping absolute file paths to file contents /// in the HEAD commit. - head_cache: HashMap, + head_cache: HashMap, } impl std::fmt::Debug for Git { @@ -54,7 +56,7 @@ impl Git { path.strip_prefix(&self.root).ok() } - pub fn read_file_from_head(&mut self, file: &Path) -> Option<&str> { + pub fn read_file_from_head(&mut self, file: &Path) -> Option<&Rope> { let current_head = Self::head_commit_id(&self.repo)?; // TODO: Check cache validity on events like WindowChange // instead of on every keypress ? Will require hooks. @@ -70,30 +72,35 @@ impl Git { let blob = object.peel_to_blob().ok()?; let contents = std::str::from_utf8(blob.content()).ok()?; self.head_cache - .insert(file.to_path_buf(), contents.to_string()); + .insert(file.to_path_buf(), Rope::from_str(contents)); } - self.head_cache.get(file).map(|s| s.as_str()) + self.head_cache.get(file) } - pub fn line_diff_with_head(&mut self, file: &Path, contents: &str) -> LineDiffs { - let base = match self.read_file_from_head(file) { - Some(b) => b, + pub fn line_diff_with_head(&mut self, file: &Path, contents: &Rope) -> LineDiffs { + let old = match self.read_file_from_head(file) { + Some(rope) => RopeLine::from_rope(rope), None => return LineDiffs::new(), }; - let mut config = similar::TextDiff::configure(); - config.timeout(std::time::Duration::from_millis(250)); + let new = RopeLine::from_rope(contents); let mut line_diffs: LineDiffs = HashMap::new(); - let mut mark_lines = |range: Range, change: LineDiff| { for line in range { line_diffs.insert(line, change); } }; - let diff = config.diff_lines(base, contents); - for op in diff.ops() { + let timeout = Duration::from_millis(250); + let diff = similar::capture_diff_slices_deadline( + similar::Algorithm::Myers, + &old, + &new, + Some(Instant::now() + timeout), + ); + + for op in diff { let (tag, _, line_range) = op.as_tag_tuple(); let start = line_range.start; match tag { @@ -173,15 +180,16 @@ mod test { exec_git_cmd("commit -m message", git_dir); let mut git = Git::discover_from_path(&file).unwrap(); + let rope = Rope::from_str(contents); assert_eq!( - Some(contents), + Some(&rope), git.read_file_from_head(&file), "Wrong blob contents from HEAD on clean index" ); fs::write(&file, "new text").expect("Could not write to file"); assert_eq!( - Some(contents), + Some(&rope), git.read_file_from_head(&file), "Wrong blob contents from HEAD when index is dirty" ); diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index a7abc9e39b15..4e3e4f4d3291 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -1,4 +1,5 @@ mod git; +mod rope; use std::{ cell::RefCell, diff --git a/helix-vcs/src/rope.rs b/helix-vcs/src/rope.rs new file mode 100644 index 000000000000..61df186cbfdb --- /dev/null +++ b/helix-vcs/src/rope.rs @@ -0,0 +1,10 @@ +use ropey::Rope; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +pub struct RopeLine<'a>(pub ropey::RopeSlice<'a>); + +impl<'a> RopeLine<'a> { + pub fn from_rope(rope: &'a Rope) -> Vec { + rope.lines().into_iter().map(RopeLine).collect() + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 7e39f029666c..806606a749ba 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -628,7 +628,7 @@ impl Document { .as_ref() .and_then(|v| v.try_borrow_mut().ok()); if let Some((mut vcs, path)) = vcs.zip(self.path()) { - self.line_diffs = vcs.line_diff_with_head(path, &self.text().to_string()); + self.line_diffs = vcs.line_diff_with_head(path, self.text()); } } From f7bcd3fb6cf00a386d73d68f678434feda42628d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 20 Mar 2022 20:28:05 +0530 Subject: [PATCH 3/3] Incrementally diff using changesets --- helix-view/src/document.rs | 68 ++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 806606a749ba..94129c489f73 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail, Context, Error}; use helix_core::auto_pairs::AutoPairs; +use helix_core::diff::compare_ropes; use helix_vcs::LineDiffs; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; @@ -122,7 +123,11 @@ pub struct Document { diagnostics: Vec, language_server: Option>, + version_control: Option>>, + /// The changes that should be applied to the current text in the doc + /// to get the diff base (eg. checked in version of the file in git). + diff_changes: Option, line_diffs: LineDiffs, } @@ -365,6 +370,7 @@ impl Document { modified_since_accessed: false, language_server: None, version_control: None, + diff_changes: None, line_diffs: LineDiffs::new(), } } @@ -623,12 +629,42 @@ impl Document { } pub fn diff_with_vcs(&mut self) { - let vcs = self + let mut vcs = self .version_control .as_ref() .and_then(|v| v.try_borrow_mut().ok()); - if let Some((mut vcs, path)) = vcs.zip(self.path()) { - self.line_diffs = vcs.line_diff_with_head(path, self.text()); + let diff_base = self + .path() + .and_then(|path| vcs.as_mut()?.read_file_from_head(path)); + if let Some(diff_base) = diff_base { + let changes = compare_ropes(diff_base, self.text()).changes().clone(); + let changes = changes.invert(diff_base); + self.diff_changes = Some(changes); + drop(vcs); + self.diff_with_base(); + } + } + + pub fn diff_with_base(&mut self) { + let changes = match &self.diff_changes { + Some(c) => c, + None => return, + }; + self.line_diffs.clear(); + + for (from, to, replacement) in changes.changes_iter() { + let from_line = self.text().char_to_line(from); + let to_line = self.text().char_to_line(to); + let mut mark_with = |diff_tag| { + for line_idx in from_line..=to_line { + self.line_diffs.entry(line_idx).or_insert(diff_tag); + } + }; + match replacement { + None if from_line == to_line => mark_with(helix_vcs::LineDiff::Modified), + None => mark_with(helix_vcs::LineDiff::Added), + Some(_) => mark_with(helix_vcs::LineDiff::Deleted), + } } } @@ -668,12 +704,32 @@ impl Document { if !transaction.changes().is_empty() { self.version += 1; + let reverted_tx = transaction.invert(&old_doc); + + // Consider H to be the text of the file in git HEAD, and B₁ to be + // the text when buffer initially loads. H → B₁ is the changeset + // that describes the diff between HEAD and buffer text. Inverting + // this produces B₁ → H, which is initially saved to `diff_changes`. + // In the next edit, buffer text changes to B₂. The transaction + // describes the change B₁ → B₂. Inverting this gives B₂ → B₁. + // Composing this with the saved `diff_changes` gives us + // (B₂ → B₁) → (B₁ → H) = B₂ → H. Whenever examining a change X₁ → X₂, + // we need to know the contents of the text at state X₁ to know where + // to apply the operations in the changeset. The advantage of inverting and + // composing this way instead of simply composing (which would give + // us H → B₂ in this example) is that we no longer need the HEAD text + // and we can instead use the current text in the buffer. + if let Some(changes) = self.diff_changes.take() { + let reverted_changes = reverted_tx.changes().clone(); + let changes = reverted_changes.compose(changes); + self.diff_changes = Some(changes); + self.diff_with_base(); + } // generate revert to savepoint if self.savepoint.is_some() { take_with(&mut self.savepoint, |prev_revert| { - let revert = transaction.invert(&old_doc); - Some(revert.compose(prev_revert.unwrap())) + Some(reverted_tx.compose(prev_revert.unwrap())) }); } @@ -707,8 +763,6 @@ impl Document { tokio::spawn(notify); } } - - self.diff_with_vcs(); } success }