diff --git a/Cargo.lock b/Cargo.lock index b32b6a47a2..31f5f30f38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2224,6 +2224,21 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "git2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glob" version = "0.3.0" @@ -2894,6 +2909,20 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libgit2-sys" +version = "0.14.0+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.3" @@ -2934,6 +2963,20 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -3970,6 +4013,7 @@ dependencies = [ "fsevent", "futures", "fuzzy", + "git2", "gpui", "ignore", "language", diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 08843aacfe..ca86f9c172 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -47,6 +47,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, + head_text: Option, file: Option>, saved_version: clock::Global, saved_version_fingerprint: String, @@ -328,17 +329,20 @@ impl Buffer { Self::build( TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), None, + None, ) } pub fn from_file>( replica_id: ReplicaId, base_text: T, + head_text: Option, file: Arc, cx: &mut ModelContext, ) -> Self { Self::build( TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), + head_text.map(|h| h.into()), Some(file), ) } @@ -349,7 +353,7 @@ impl Buffer { file: Option>, ) -> Result { let buffer = TextBuffer::new(replica_id, message.id, message.base_text); - let mut this = Self::build(buffer, file); + let mut this = Self::build(buffer, message.head_text, file); this.text.set_line_ending(proto::deserialize_line_ending( proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, @@ -362,6 +366,7 @@ impl Buffer { id: self.remote_id(), file: self.file.as_ref().map(|f| f.to_proto()), base_text: self.base_text().to_string(), + head_text: self.head_text.clone(), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, } } @@ -404,7 +409,7 @@ impl Buffer { self } - fn build(buffer: TextBuffer, file: Option>) -> Self { + fn build(buffer: TextBuffer, head_text: Option, file: Option>) -> Self { let saved_mtime = if let Some(file) = file.as_ref() { file.mtime() } else { @@ -418,6 +423,7 @@ impl Buffer { transaction_depth: 0, was_dirty_before_starting_transaction: None, text: buffer, + head_text, file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index a4ea6f2286..4e7ff2d471 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -52,6 +52,7 @@ smol = "1.2.5" thiserror = "1.0.29" toml = "0.5" rocksdb = "0.18" +git2 = "0.15" [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index f2d62fae87..68d07c891c 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,9 +1,11 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; +use git2::{Repository, RepositoryOpenFlags}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ + ffi::OsStr, io, os::unix::fs::MetadataExt, path::{Component, Path, PathBuf}, @@ -29,6 +31,7 @@ pub trait Fs: Send + Sync { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; + async fn load_head_text(&self, path: &Path) -> Option; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; @@ -161,6 +164,38 @@ impl Fs for RealFs { Ok(text) } + async fn load_head_text(&self, path: &Path) -> Option { + fn logic(path: &Path) -> Result> { + let repo = Repository::open_ext(path, RepositoryOpenFlags::empty(), &[OsStr::new("")])?; + assert!(repo.path().ends_with(".git")); + let repo_root_path = match repo.path().parent() { + Some(root) => root, + None => return Ok(None), + }; + + let relative_path = path.strip_prefix(repo_root_path)?; + let object = repo + .head()? + .peel_to_tree()? + .get_path(relative_path)? + .to_object(&repo)?; + + let content = match object.as_blob() { + Some(blob) => blob.content().to_owned(), + None => return Ok(None), + }; + + let head_text = String::from_utf8(content.to_owned())?; + Ok(Some(head_text)) + } + + match logic(path) { + Ok(value) => return value, + Err(err) => log::error!("Error loading head text: {:?}", err), + } + None + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); let file = smol::fs::File::create(path).await?; @@ -748,6 +783,10 @@ impl Fs for FakeFs { entry.file_content(&path).cloned() } + async fn load_head_text(&self, _: &Path) -> Option { + None + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 74c50e0c5f..42d18eb3bb 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -446,10 +446,10 @@ impl LocalWorktree { ) -> Task>> { let path = Arc::from(path); cx.spawn(move |this, mut cx| async move { - let (file, contents) = this + let (file, contents, head_text) = this .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) .await?; - Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx))) + Ok(cx.add_model(|cx| Buffer::from_file(0, contents, head_text, Arc::new(file), cx))) }) } @@ -558,13 +558,19 @@ impl LocalWorktree { } } - fn load(&self, path: &Path, cx: &mut ModelContext) -> Task> { + fn load( + &self, + path: &Path, + cx: &mut ModelContext, + ) -> Task)>> { let handle = cx.handle(); let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; + let head_text = fs.load_head_text(&abs_path).await; + // Eagerly populate the snapshot with an updated entry for the loaded file let entry = this .update(&mut cx, |this, cx| { @@ -573,6 +579,7 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None, cx) }) .await?; + Ok(( File { entry_id: Some(entry.id), @@ -582,6 +589,7 @@ impl LocalWorktree { is_local: true, }, text, + head_text, )) }) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7840829b44..818f2cb7e1 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -821,7 +821,8 @@ message BufferState { uint64 id = 1; optional File file = 2; string base_text = 3; - LineEnding line_ending = 4; + optional string head_text = 4; + LineEnding line_ending = 5; } message BufferChunk {