From 2a14af4cdea11693943377964e613ed2b61d664c Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Tue, 30 Aug 2022 11:08:22 -0400 Subject: [PATCH 01/73] Load a file's head text on file load just to get started --- Cargo.lock | 44 ++++++++++++++++++++++++++++++++++ crates/language/src/buffer.rs | 10 ++++++-- crates/project/Cargo.toml | 1 + crates/project/src/fs.rs | 39 ++++++++++++++++++++++++++++++ crates/project/src/worktree.rs | 14 ++++++++--- crates/rpc/proto/zed.proto | 3 ++- 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b32b6a47a27539bbc1b244f95819aa0b82e1e5bd..31f5f30f38bbd04c74c9157ee57c65ee6abe002d 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 08843aacfe2e0a5babf44706c980692c3ae9b198..ca86f9c1723601ab072c97bed0242c6daa2da941 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 a4ea6f22864df3a51a99d59994fe0755bbaf576a..4e7ff2d471dfcd83101581476f8618d96e3a67cf 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 f2d62fae87e54b84ffb916e8764a5e07871d575d..68d07c891cf16c912bff76cf872089b500b63201 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 74c50e0c5fc6d4a6a5d8f949e0d587ad604ee50d..42d18eb3bbdba06ff80b35a51bf2ca9aa4632031 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 7840829b4461bc7ca497f7bf1bcc7b34e397f0f8..818f2cb7e1c009c2b2d5df50a2bb83dc9462303a 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 { From 6fa2e62fa41175668fb62479063117682c9ecde9 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 30 Aug 2022 16:29:20 -0400 Subject: [PATCH 02/73] Start asking Editors to update git after a debounced delay --- crates/editor/src/items.rs | 9 ++ crates/workspace/src/workspace.rs | 132 ++++++++++++++++++++++++------ 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f63ffc3d7cce0eb920818b36747961290d2865b2..22a069c5c0e32f305398d3794e915e47449a083d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -478,6 +478,15 @@ impl Item for Editor { }) } + fn update_git( + &mut self, + _project: ModelHandle, + _cx: &mut ViewContext, + ) -> Task> { + println!("Editor::update_git"); + Task::ready(Ok(())) + } + fn to_item_events(event: &Self::Event) -> Vec { let mut result = Vec::new(); match event { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b9cface656b6d35060d2c74c6111cf3c9f36faa1..3446dc0f0eaa07e9e9a30eb411f0e4871d2fedcc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -52,7 +52,6 @@ use std::{ cell::RefCell, fmt, future::Future, - mem, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -318,7 +317,23 @@ pub trait Item: View { project: ModelHandle, cx: &mut ViewContext, ) -> Task>; + fn update_git( + &mut self, + _project: ModelHandle, + _cx: &mut ViewContext, + ) -> Task> { + Task::ready(Ok(())) + } fn to_item_events(event: &Self::Event) -> Vec; + fn should_close_item_on_event(_: &Self::Event) -> bool { + false + } + fn should_update_tab_on_event(_: &Self::Event) -> bool { + false + } + fn is_edit_event(_: &Self::Event) -> bool { + false + } fn act_as_type( &self, type_id: TypeId, @@ -435,6 +450,57 @@ impl FollowableItemHandle for ViewHandle { } } +struct DelayedDebouncedEditAction { + task: Option>, + cancel_channel: Option>, +} + +impl DelayedDebouncedEditAction { + fn new() -> DelayedDebouncedEditAction { + DelayedDebouncedEditAction { + task: None, + cancel_channel: None, + } + } + + fn fire_new( + &mut self, + delay: Duration, + workspace: &Workspace, + cx: &mut ViewContext, + f: F, + ) where + F: FnOnce(ModelHandle, AsyncAppContext) -> Fut + 'static, + Fut: 'static + Future, + { + if let Some(channel) = self.cancel_channel.take() { + _ = channel.send(()); + } + + let project = workspace.project().downgrade(); + + let (sender, mut receiver) = oneshot::channel::<()>(); + self.cancel_channel = Some(sender); + + let previous_task = self.task.take(); + self.task = Some(cx.spawn_weak(|_, cx| async move { + let mut timer = cx.background().timer(delay).fuse(); + if let Some(previous_task) = previous_task { + previous_task.await; + } + + futures::select_biased! { + _ = receiver => return, + _ = timer => {} + } + + if let Some(project) = project.upgrade(&cx) { + (f)(project, cx).await; + } + })); + } +} + pub trait ItemHandle: 'static + fmt::Debug { fn subscribe_to_item_events( &self, @@ -473,6 +539,11 @@ pub trait ItemHandle: 'static + fmt::Debug { ) -> Task>; fn reload(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task>; + fn update_git( + &self, + project: ModelHandle, + cx: &mut MutableAppContext, + ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( @@ -578,8 +649,8 @@ impl ItemHandle for ViewHandle { .insert(self.id(), pane.downgrade()) .is_none() { - let mut pending_autosave = None; - let mut cancel_pending_autosave = oneshot::channel::<()>().0; + let mut pending_autosave = DelayedDebouncedEditAction::new(); + let mut pending_git_update = DelayedDebouncedEditAction::new(); let pending_update = Rc::new(RefCell::new(None)); let pending_update_scheduled = Rc::new(AtomicBool::new(false)); @@ -637,45 +708,46 @@ impl ItemHandle for ViewHandle { .detach_and_log_err(cx); return; } + ItemEvent::UpdateTab => { pane.update(cx, |_, cx| { cx.emit(pane::Event::ChangeItemTitle); cx.notify(); }); } + ItemEvent::Edit => { if let Autosave::AfterDelay { milliseconds } = cx.global::().autosave { - let prev_autosave = pending_autosave - .take() - .unwrap_or_else(|| Task::ready(Some(()))); - let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>(); - let prev_cancel_tx = - mem::replace(&mut cancel_pending_autosave, cancel_tx); - let project = workspace.project.downgrade(); - let _ = prev_cancel_tx.send(()); + let delay = Duration::from_millis(milliseconds); let item = item.clone(); - pending_autosave = - Some(cx.spawn_weak(|_, mut cx| async move { - let mut timer = cx - .background() - .timer(Duration::from_millis(milliseconds)) - .fuse(); - prev_autosave.await; - futures::select_biased! { - _ = cancel_rx => return None, - _ = timer => {} - } - - let project = project.upgrade(&cx)?; + pending_autosave.fire_new( + delay, + workspace, + cx, + |project, mut cx| async move { cx.update(|cx| Pane::autosave_item(&item, project, cx)) .await .log_err(); - None - })); + }, + ); } + + const GIT_DELAY: Duration = Duration::from_millis(800); + let item = item.clone(); + pending_git_update.fire_new( + GIT_DELAY, + workspace, + cx, + |project, mut cx| async move { + cx.update(|cx| item.update_git(project, cx)) + .await + .log_err(); + }, + ); } + _ => {} } } @@ -755,6 +827,14 @@ impl ItemHandle for ViewHandle { self.update(cx, |item, cx| item.reload(project, cx)) } + fn update_git( + &self, + project: ModelHandle, + cx: &mut MutableAppContext, + ) -> Task> { + self.update(cx, |item, cx| item.update_git(project, cx)) + } + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { self.read(cx).act_as_type(type_id, self, cx) } From 55ca02351c5e8c662f7f9e09a8ce93a4f69233c5 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 1 Sep 2022 17:22:12 -0400 Subject: [PATCH 03/73] Start painting some sort of hunk info, it's wrong but it's close Co-Authored-By: Max Brunsfeld --- Cargo.lock | 2 +- crates/editor/src/element.rs | 35 ++++++ crates/editor/src/multi_buffer.rs | 16 ++- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 35 ++++++ crates/language/src/git.rs | 197 ++++++++++++++++++++++++++++++ crates/language/src/language.rs | 1 + crates/project/Cargo.toml | 1 - crates/project/src/fs.rs | 2 +- crates/project/src/project.rs | 4 +- 10 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 crates/language/src/git.rs diff --git a/Cargo.lock b/Cargo.lock index 31f5f30f38bbd04c74c9157ee57c65ee6abe002d..2872d83a943df07d9bdeff6f8d1f6d336d9eba15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,6 +2855,7 @@ dependencies = [ "env_logger", "futures", "fuzzy", + "git2", "gpui", "lazy_static", "log", @@ -4013,7 +4014,6 @@ dependencies = [ "fsevent", "futures", "fuzzy", - "git2", "gpui", "ignore", "language", diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1e1ab83063bfc31f9aa7aa964147007fa9e895ed..357f15432bed8b51c165311f2b4225fdbe46fd84 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -34,6 +34,7 @@ use gpui::{ WeakViewHandle, }; use json::json; +use language::git::{DiffHunk, DiffHunkStatus}; use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; use settings::Settings; @@ -543,6 +544,33 @@ impl EditorElement { } } + println!("painting from hunks: {:#?}\n", &layout.diff_hunks); + for hunk in &layout.diff_hunks { + let color = match hunk.status() { + DiffHunkStatus::Added => Color::green(), + DiffHunkStatus::Modified => Color::blue(), + _ => continue, + }; + + let start_row = hunk.buffer_range.start; + let end_row = hunk.buffer_range.end; + + let start_y = start_row as f32 * layout.line_height - (scroll_top % layout.line_height); + let end_y = end_row as f32 * layout.line_height - (scroll_top % layout.line_height) + + layout.line_height; + + let highlight_origin = bounds.origin() + vec2f(0., start_y); + let highlight_size = vec2f(6., end_y - start_y); + let highlight_bounds = RectF::new(highlight_origin, highlight_size); + + cx.scene.push_quad(Quad { + bounds: highlight_bounds, + background: Some(color), + border: Border::new(0., Color::transparent_black()), + corner_radius: 0., + }); + } + if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; let mut y = *row as f32 * layout.position_map.line_height - scroll_top; @@ -1425,6 +1453,11 @@ impl Element for EditorElement { let line_number_layouts = self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx); + let diff_hunks = snapshot + .buffer_snapshot + .diff_hunks_in_range(start_row..end_row) + .collect(); + let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); for line in &line_layouts { @@ -1573,6 +1606,7 @@ impl Element for EditorElement { highlighted_rows, highlighted_ranges, line_number_layouts, + diff_hunks, blocks, selections, context_menu, @@ -1710,6 +1744,7 @@ pub struct LayoutState { highlighted_ranges: Vec<(Range, Color)>, selections: Vec<(ReplicaId, Vec)>, context_menu: Option<(DisplayPoint, ElementBox)>, + diff_hunks: Vec>, code_actions_indicator: Option<(u32, ElementBox)>, hover_popovers: Option<(DisplayPoint, Vec)>, } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4ee9526a6797a2d84bfe5776a72a940b9f71466e..91de32bac9e7d487fe8f524d41b0093292abcd3e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,9 +7,10 @@ use collections::{BTreeMap, Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, - DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, - Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, git::DiffHunk, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, + Chunk, DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, + OutlineItem, Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, + TransactionId, }; use smallvec::SmallVec; use std::{ @@ -2529,6 +2530,15 @@ impl MultiBufferSnapshot { }) } + pub fn diff_hunks_in_range<'a>( + &'a self, + row_range: Range, + ) -> impl 'a + Iterator> { + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| buffer.diff_hunks_in_range(row_range.clone())) + } + pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6e9f368e77be909c1a8fb2d149be679b3aa0b66b..6d347f3595312ab8415ee123b56823df3fadee6a 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -51,6 +51,7 @@ smol = "1.2" tree-sitter = "0.20" tree-sitter-rust = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true } +git2 = "0.15" [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ca86f9c1723601ab072c97bed0242c6daa2da941..ad3d8978adf31b1f7039775be9438331028f44e5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,3 +1,4 @@ +use crate::git::{BufferDiff, DiffHunk}; pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, @@ -48,6 +49,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, head_text: Option, + diff: BufferDiff, file: Option>, saved_version: clock::Global, saved_version_fingerprint: String, @@ -74,6 +76,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, + git_hunks: Arc<[DiffHunk]>, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -416,6 +419,11 @@ impl Buffer { UNIX_EPOCH }; + let mut diff = BufferDiff::new(); + if let Some(head_text) = &head_text { + diff.update(head_text, &buffer); + } + Self { saved_mtime, saved_version: buffer.version(), @@ -424,6 +432,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, head_text, + diff, file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, @@ -453,6 +462,7 @@ impl Buffer { BufferSnapshot { text, syntax, + git_hunks: self.diff.hunks(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -2145,6 +2155,30 @@ impl BufferSnapshot { }) } + pub fn diff_hunks_in_range<'a>( + &'a self, + query_row_range: Range, + ) -> impl 'a + Iterator> { + self.git_hunks.iter().filter_map(move |hunk| { + let range = hunk.buffer_range.to_point(&self.text); + + if range.start.row < query_row_range.end && query_row_range.start < range.end.row { + let end_row = if range.end.column > 0 { + range.end.row + 1 + } else { + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + head_range: hunk.head_range.clone(), + }) + } else { + None + } + }) + } + pub fn diagnostics_in_range<'a, T, O>( &'a self, search_range: Range, @@ -2218,6 +2252,7 @@ impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { text: self.text.clone(), + git_hunks: self.git_hunks.clone(), syntax: self.syntax.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..5445396918fcad5390ab0dd49e4a0a98a23e8ee2 --- /dev/null +++ b/crates/language/src/git.rs @@ -0,0 +1,197 @@ +use std::ops::Range; +use std::sync::Arc; + +use sum_tree::Bias; +use text::{Anchor, Point}; + +pub use git2 as libgit; +use libgit::Patch as GitPatch; + +#[derive(Debug, Clone, Copy)] +pub enum DiffHunkStatus { + Added, + Modified, + Removed, +} + +#[derive(Debug)] +pub struct DiffHunk { + pub buffer_range: Range, + pub head_range: Range, +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.head_range.is_empty() { + DiffHunkStatus::Added + } else if self.buffer_range.is_empty() { + DiffHunkStatus::Removed + } else { + DiffHunkStatus::Modified + } + } +} + +pub struct BufferDiff { + hunks: Arc<[DiffHunk]>, +} + +impl BufferDiff { + pub fn new() -> BufferDiff { + BufferDiff { + hunks: Arc::new([]), + } + } + + pub fn hunks(&self) -> Arc<[DiffHunk]> { + self.hunks.clone() + } + + pub fn update(&mut self, head: &str, buffer: &text::BufferSnapshot) { + let current = buffer.as_rope().to_string().into_bytes(); + let patch = match GitPatch::from_buffers(head.as_bytes(), None, ¤t, None, None) { + Ok(patch) => patch, + Err(_) => { + //Reset hunks in case of failure to avoid showing a stale (potentially erroneous) diff + self.hunks = Arc::new([]); + return; + } + }; + + let mut hunks = Vec::new(); + for index in 0..patch.num_hunks() { + let (hunk, _) = match patch.hunk(index) { + Ok(it) => it, + Err(_) => break, + }; + + let new_start = hunk.new_start(); + let new_end = new_start + hunk.new_lines(); + let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); + let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); + let buffer_range = start_anchor..end_anchor; + + let old_start = hunk.old_start() as usize; + let old_end = old_start + hunk.old_lines() as usize; + let head_range = old_start..old_end; + + hunks.push(DiffHunk { + buffer_range, + head_range, + }); + } + + self.hunks = hunks.into(); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum GitDiffEdit { + Added(u32), + Modified(u32), + Removed(u32), +} + +impl GitDiffEdit { + pub fn line(self) -> u32 { + use GitDiffEdit::*; + + match self { + Added(line) | Modified(line) | Removed(line) => line, + } + } +} + +// struct DiffTracker { +// track_line_num: u32, +// edits: Vec, +// } + +// impl DiffTracker { +// fn new() -> DiffTracker { +// DiffTracker { +// track_line_num: 0, +// edits: Vec::new(), +// } +// } + +// fn attempt_finalize_file(&mut self, base_path: &Path) -> Result<()> { +// let relative = if let Some(relative) = self.last_file_path.clone() { +// relative +// } else { +// return Ok(()); +// }; + +// let mut path = base_path.to_path_buf(); +// path.push(relative); +// path = canonicalize(path).map_err(Error::Io)?; + +// self.diffs.push(GitFileDiff { +// path, +// edits: take(&mut self.edits), +// }); + +// Ok(()) +// } + +// fn handle_diff_line( +// &mut self, +// delta: DiffDelta, +// line: DiffLine, +// base_path: &Path, +// ) -> Result<()> { +// let path = match (delta.old_file().path(), delta.new_file().path()) { +// (Some(old), _) => old, +// (_, Some(new)) => new, +// (_, _) => return Err(Error::DeltaMissingPath), +// }; + +// if self.last_file_path.as_deref() != Some(path) { +// self.attempt_finalize_file(base_path)?; +// self.last_file_path = Some(path.to_path_buf()); +// self.track_line_num = 0; +// } + +// match line.origin_value() { +// DiffLineType::Context => { +// self.track_line_num = line.new_lineno().ok_or(Error::ContextMissingLineNum)?; +// } + +// DiffLineType::Deletion => { +// self.track_line_num += 1; +// self.edits.push(GitDiffEdit::Removed(self.track_line_num)); +// } + +// DiffLineType::Addition => { +// let addition_line_num = line.new_lineno().ok_or(Error::AdditionMissingLineNum)?; +// self.track_line_num = addition_line_num; + +// let mut replaced = false; +// for rewind_index in (0..self.edits.len()).rev() { +// let edit = &mut self.edits[rewind_index]; + +// if let GitDiffEdit::Removed(removed_line_num) = *edit { +// match removed_line_num.cmp(&addition_line_num) { +// Ordering::Equal => { +// *edit = GitDiffEdit::Modified(addition_line_num); +// replaced = true; +// break; +// } + +// Ordering::Greater => continue, +// Ordering::Less => break, +// } +// } +// } + +// if !replaced { +// self.edits.push(GitDiffEdit::Added(addition_line_num)); +// } +// } + +// _ => {} +// } + +// Ok(()) +// } +// } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 780f6e75b52521bf751f7de1a748b91954001ac3..8e2fe601e7bf105056699e18018fb5f8246fa1eb 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,5 +1,6 @@ mod buffer; mod diagnostic_set; +pub mod git; mod highlight_map; mod outline; pub mod proto; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 4e7ff2d471dfcd83101581476f8618d96e3a67cf..a4ea6f22864df3a51a99d59994fe0755bbaf576a 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -52,7 +52,6 @@ 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 68d07c891cf16c912bff76cf872089b500b63201..a983df0f4b4eb20de30c945235b506883344bc37 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git2::{Repository, RepositoryOpenFlags}; +use language::git::libgit::{Repository, RepositoryOpenFlags}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6841c561d01bc0cfb21b3e6301934ef80829da50..8fa1fe962289935321cba03015339c987277dd47 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4533,8 +4533,8 @@ impl Project { fn add_worktree(&mut self, worktree: &ModelHandle, cx: &mut ModelContext) { cx.observe(worktree, |_, _, cx| cx.notify()).detach(); if worktree.read(cx).is_local() { - cx.subscribe(worktree, |this, worktree, _, cx| { - this.update_local_worktree_buffers(worktree, cx); + cx.subscribe(worktree, |this, worktree, event, cx| match event { + worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), }) .detach(); } From 641daf0a6eddd3b852886e73ee7a450f3a40359c Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 2 Sep 2022 10:39:32 -0400 Subject: [PATCH 04/73] Correct git gutter indicator scroll position & add rounded corner --- crates/editor/src/element.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 357f15432bed8b51c165311f2b4225fdbe46fd84..c82860cb729624635d743b601b606aabc0e4adb0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -544,7 +544,7 @@ impl EditorElement { } } - println!("painting from hunks: {:#?}\n", &layout.diff_hunks); + println!("painting from hunks: {:#?}\n", layout.diff_hunks); for hunk in &layout.diff_hunks { let color = match hunk.status() { DiffHunkStatus::Added => Color::green(), @@ -555,19 +555,19 @@ impl EditorElement { let start_row = hunk.buffer_range.start; let end_row = hunk.buffer_range.end; - let start_y = start_row as f32 * layout.line_height - (scroll_top % layout.line_height); - let end_y = end_row as f32 * layout.line_height - (scroll_top % layout.line_height) - + layout.line_height; + let start_y = start_row as f32 * layout.line_height - scroll_top; + let end_y = end_row as f32 * layout.line_height + layout.line_height - scroll_top; - let highlight_origin = bounds.origin() + vec2f(0., start_y); - let highlight_size = vec2f(6., end_y - start_y); + let width = 0.22 * layout.line_height; + let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); cx.scene.push_quad(Quad { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: 0., + corner_radius: 0.2 * layout.line_height, }); } From fdda2abb782a36075c825f91f774bf97386726f7 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 2 Sep 2022 14:35:35 -0400 Subject: [PATCH 05/73] Correct start/end of git diff hunks --- crates/editor/src/element.rs | 4 +- crates/language/src/git.rs | 108 +++-------------------------------- 2 files changed, 11 insertions(+), 101 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c82860cb729624635d743b601b606aabc0e4adb0..b13a2a6ada8f273f99d1dbc40a201462468b84ce 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -549,14 +549,14 @@ impl EditorElement { let color = match hunk.status() { DiffHunkStatus::Added => Color::green(), DiffHunkStatus::Modified => Color::blue(), - _ => continue, + DiffHunkStatus::Removed => continue, }; let start_row = hunk.buffer_range.start; let end_row = hunk.buffer_range.end; let start_y = start_row as f32 * layout.line_height - scroll_top; - let end_y = end_row as f32 * layout.line_height + layout.line_height - scroll_top; + let end_y = end_row as f32 * layout.line_height - scroll_top; let width = 0.22 * layout.line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 5445396918fcad5390ab0dd49e4a0a98a23e8ee2..73e511ca482c6368c7559943df554a5d66031d2d 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -5,7 +5,7 @@ use sum_tree::Bias; use text::{Anchor, Point}; pub use git2 as libgit; -use libgit::Patch as GitPatch; +use libgit::{DiffOptions as GitOptions, Patch as GitPatch}; #[derive(Debug, Clone, Copy)] pub enum DiffHunkStatus { @@ -48,8 +48,12 @@ impl BufferDiff { } pub fn update(&mut self, head: &str, buffer: &text::BufferSnapshot) { + let head = head.as_bytes(); let current = buffer.as_rope().to_string().into_bytes(); - let patch = match GitPatch::from_buffers(head.as_bytes(), None, ¤t, None, None) { + + let mut options = GitOptions::default(); + options.context_lines(0); + let patch = match GitPatch::from_buffers(head, None, ¤t, None, Some(&mut options)) { Ok(patch) => patch, Err(_) => { //Reset hunks in case of failure to avoid showing a stale (potentially erroneous) diff @@ -62,16 +66,16 @@ impl BufferDiff { for index in 0..patch.num_hunks() { let (hunk, _) = match patch.hunk(index) { Ok(it) => it, - Err(_) => break, + Err(_) => continue, }; - let new_start = hunk.new_start(); + let new_start = hunk.new_start() - 1; let new_end = new_start + hunk.new_lines(); let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); let buffer_range = start_anchor..end_anchor; - let old_start = hunk.old_start() as usize; + let old_start = hunk.old_start() as usize - 1; let old_end = old_start + hunk.old_lines() as usize; let head_range = old_start..old_end; @@ -101,97 +105,3 @@ impl GitDiffEdit { } } } - -// struct DiffTracker { -// track_line_num: u32, -// edits: Vec, -// } - -// impl DiffTracker { -// fn new() -> DiffTracker { -// DiffTracker { -// track_line_num: 0, -// edits: Vec::new(), -// } -// } - -// fn attempt_finalize_file(&mut self, base_path: &Path) -> Result<()> { -// let relative = if let Some(relative) = self.last_file_path.clone() { -// relative -// } else { -// return Ok(()); -// }; - -// let mut path = base_path.to_path_buf(); -// path.push(relative); -// path = canonicalize(path).map_err(Error::Io)?; - -// self.diffs.push(GitFileDiff { -// path, -// edits: take(&mut self.edits), -// }); - -// Ok(()) -// } - -// fn handle_diff_line( -// &mut self, -// delta: DiffDelta, -// line: DiffLine, -// base_path: &Path, -// ) -> Result<()> { -// let path = match (delta.old_file().path(), delta.new_file().path()) { -// (Some(old), _) => old, -// (_, Some(new)) => new, -// (_, _) => return Err(Error::DeltaMissingPath), -// }; - -// if self.last_file_path.as_deref() != Some(path) { -// self.attempt_finalize_file(base_path)?; -// self.last_file_path = Some(path.to_path_buf()); -// self.track_line_num = 0; -// } - -// match line.origin_value() { -// DiffLineType::Context => { -// self.track_line_num = line.new_lineno().ok_or(Error::ContextMissingLineNum)?; -// } - -// DiffLineType::Deletion => { -// self.track_line_num += 1; -// self.edits.push(GitDiffEdit::Removed(self.track_line_num)); -// } - -// DiffLineType::Addition => { -// let addition_line_num = line.new_lineno().ok_or(Error::AdditionMissingLineNum)?; -// self.track_line_num = addition_line_num; - -// let mut replaced = false; -// for rewind_index in (0..self.edits.len()).rev() { -// let edit = &mut self.edits[rewind_index]; - -// if let GitDiffEdit::Removed(removed_line_num) = *edit { -// match removed_line_num.cmp(&addition_line_num) { -// Ordering::Equal => { -// *edit = GitDiffEdit::Modified(addition_line_num); -// replaced = true; -// break; -// } - -// Ordering::Greater => continue, -// Ordering::Less => break, -// } -// } -// } - -// if !replaced { -// self.edits.push(GitDiffEdit::Added(addition_line_num)); -// } -// } - -// _ => {} -// } - -// Ok(()) -// } -// } From 5157c71fa9f825c0d0fd48e5b7015f6baafa86ae Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 2 Sep 2022 15:22:15 -0400 Subject: [PATCH 06/73] Render deletion gutter markers --- crates/editor/src/element.rs | 42 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b13a2a6ada8f273f99d1dbc40a201462468b84ce..4ee14407b893909d46e1d0894a51c40f9e17d78e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -525,8 +525,9 @@ impl EditorElement { layout: &mut LayoutState, cx: &mut PaintContext, ) { - let scroll_top = - layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height; + let line_height = layout.position_map.line_height; + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y() * line_height; for (ix, line) in layout.line_number_layouts.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin() @@ -544,21 +545,42 @@ impl EditorElement { } } - println!("painting from hunks: {:#?}\n", layout.diff_hunks); for hunk in &layout.diff_hunks { let color = match hunk.status() { DiffHunkStatus::Added => Color::green(), DiffHunkStatus::Modified => Color::blue(), - DiffHunkStatus::Removed => continue, + + //TODO: This rendering is entirely a horrible hack + DiffHunkStatus::Removed => { + let row_above = hunk.buffer_range.start; + + let offset = line_height / 2.; + let start_y = row_above as f32 * line_height + offset - scroll_top; + let end_y = start_y + line_height; + + let width = 0.4 * line_height; + let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let highlight_size = vec2f(width * 2., end_y - start_y); + let highlight_bounds = RectF::new(highlight_origin, highlight_size); + + cx.scene.push_quad(Quad { + bounds: highlight_bounds, + background: Some(Color::red()), + border: Border::new(0., Color::transparent_black()), + corner_radius: 1. * line_height, + }); + + continue; + } }; let start_row = hunk.buffer_range.start; let end_row = hunk.buffer_range.end; - let start_y = start_row as f32 * layout.line_height - scroll_top; - let end_y = end_row as f32 * layout.line_height - scroll_top; + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row as f32 * line_height - scroll_top; - let width = 0.22 * layout.line_height; + let width = 0.22 * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); @@ -567,15 +589,15 @@ impl EditorElement { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: 0.2 * layout.line_height, + corner_radius: 0.2 * line_height, }); } if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; - let mut y = *row as f32 * layout.position_map.line_height - scroll_top; + let mut y = *row as f32 * line_height - scroll_top; x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; - y += (layout.position_map.line_height - indicator.size().y()) / 2.; + y += (line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } } From 883d5b7a081d640afe7afcfd3729e610ecb874dc Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 6 Sep 2022 17:09:47 -0400 Subject: [PATCH 07/73] Update git gutter status after debounced delay Co-authored-by: Max Brunsfeld --- crates/editor/src/display_map/fold_map.rs | 1 + crates/editor/src/items.rs | 7 +++-- crates/editor/src/multi_buffer.rs | 26 ++++++++++++++++++ crates/language/src/buffer.rs | 33 ++++++++++++++++++----- crates/workspace/src/workspace.rs | 2 +- 5 files changed, 60 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 970910f969ee42fc30048b80fd5735ffa971ca30..6ab5c6202ea3637db18ddf9fff31780983634ce8 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -274,6 +274,7 @@ impl FoldMap { if buffer.edit_count() != new_buffer.edit_count() || buffer.parse_count() != new_buffer.parse_count() || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count() + || buffer.diff_update_count() != new_buffer.diff_update_count() || buffer.trailing_excerpt_update_count() != new_buffer.trailing_excerpt_update_count() { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 22a069c5c0e32f305398d3794e915e47449a083d..d208fc9c15819a1ffee304f585bd5dc857b839bb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -481,9 +481,12 @@ impl Item for Editor { fn update_git( &mut self, _project: ModelHandle, - _cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - println!("Editor::update_git"); + self.buffer().update(cx, |multibuffer, cx| { + multibuffer.update_git(cx); + }); + cx.notify(); Task::ready(Ok(())) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 91de32bac9e7d487fe8f524d41b0093292abcd3e..1d09b7008fbd20fd59fb29b80741cada2306216d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -91,6 +91,7 @@ struct BufferState { last_selections_update_count: usize, last_diagnostics_update_count: usize, last_file_update_count: usize, + last_diff_update_count: usize, excerpts: Vec, _subscriptions: [gpui::Subscription; 2], } @@ -102,6 +103,7 @@ pub struct MultiBufferSnapshot { parse_count: usize, diagnostics_update_count: usize, trailing_excerpt_update_count: usize, + diff_update_count: usize, edit_count: usize, is_dirty: bool, has_conflict: bool, @@ -203,6 +205,7 @@ impl MultiBuffer { last_selections_update_count: buffer_state.last_selections_update_count, last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, last_file_update_count: buffer_state.last_file_update_count, + last_diff_update_count: buffer_state.last_diff_update_count, excerpts: buffer_state.excerpts.clone(), _subscriptions: [ new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), @@ -309,6 +312,15 @@ impl MultiBuffer { self.read(cx).symbols_containing(offset, theme) } + pub fn update_git(&mut self, cx: &mut ModelContext) { + let mut buffers = self.buffers.borrow_mut(); + for buffer in buffers.values_mut() { + buffer.buffer.update(cx, |buffer, _| { + buffer.update_git(); + }) + } + } + pub fn edit( &mut self, edits: I, @@ -828,6 +840,7 @@ impl MultiBuffer { last_selections_update_count: buffer_snapshot.selections_update_count(), last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(), last_file_update_count: buffer_snapshot.file_update_count(), + last_diff_update_count: buffer_snapshot.diff_update_count(), excerpts: Default::default(), _subscriptions: [ cx.observe(&buffer, |_, _, cx| cx.notify()), @@ -1250,6 +1263,7 @@ impl MultiBuffer { let mut excerpts_to_edit = Vec::new(); let mut reparsed = false; let mut diagnostics_updated = false; + let mut diff_updated = false; let mut is_dirty = false; let mut has_conflict = false; let mut edited = false; @@ -1261,6 +1275,7 @@ impl MultiBuffer { let selections_update_count = buffer.selections_update_count(); let diagnostics_update_count = buffer.diagnostics_update_count(); let file_update_count = buffer.file_update_count(); + let diff_update_count = buffer.diff_update_count(); let buffer_edited = version.changed_since(&buffer_state.last_version); let buffer_reparsed = parse_count > buffer_state.last_parse_count; @@ -1269,17 +1284,20 @@ impl MultiBuffer { let buffer_diagnostics_updated = diagnostics_update_count > buffer_state.last_diagnostics_update_count; let buffer_file_updated = file_update_count > buffer_state.last_file_update_count; + let buffer_diff_updated = diff_update_count > buffer_state.last_diff_update_count; if buffer_edited || buffer_reparsed || buffer_selections_updated || buffer_diagnostics_updated || buffer_file_updated + || buffer_diff_updated { buffer_state.last_version = version; buffer_state.last_parse_count = parse_count; buffer_state.last_selections_update_count = selections_update_count; buffer_state.last_diagnostics_update_count = diagnostics_update_count; buffer_state.last_file_update_count = file_update_count; + buffer_state.last_diff_update_count = diff_update_count; excerpts_to_edit.extend( buffer_state .excerpts @@ -1291,6 +1309,7 @@ impl MultiBuffer { edited |= buffer_edited; reparsed |= buffer_reparsed; diagnostics_updated |= buffer_diagnostics_updated; + diff_updated |= buffer_diff_updated; is_dirty |= buffer.is_dirty(); has_conflict |= buffer.has_conflict(); } @@ -1303,6 +1322,9 @@ impl MultiBuffer { if diagnostics_updated { snapshot.diagnostics_update_count += 1; } + if diff_updated { + snapshot.diff_update_count += 1; + } snapshot.is_dirty = is_dirty; snapshot.has_conflict = has_conflict; @@ -2480,6 +2502,10 @@ impl MultiBufferSnapshot { self.diagnostics_update_count } + pub fn diff_update_count(&self) -> usize { + self.diff_update_count + } + pub fn trailing_excerpt_update_count(&self) -> usize { self.trailing_excerpt_update_count } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ad3d8978adf31b1f7039775be9438331028f44e5..10d7fa553510b5596ad68430285187f42d0a1dee 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -49,7 +49,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, head_text: Option, - diff: BufferDiff, + git_diff: BufferDiff, file: Option>, saved_version: clock::Global, saved_version_fingerprint: String, @@ -69,6 +69,7 @@ pub struct Buffer { diagnostics_update_count: usize, diagnostics_timestamp: clock::Lamport, file_update_count: usize, + diff_update_count: usize, completion_triggers: Vec, completion_triggers_timestamp: clock::Lamport, deferred_ops: OperationQueue, @@ -76,12 +77,13 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - git_hunks: Arc<[DiffHunk]>, + pub git_hunks: Arc<[DiffHunk]>, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, diagnostics_update_count: usize, file_update_count: usize, + diff_update_count: usize, remote_selections: TreeMap, selections_update_count: usize, language: Option>, @@ -419,9 +421,9 @@ impl Buffer { UNIX_EPOCH }; - let mut diff = BufferDiff::new(); + let mut git_diff = BufferDiff::new(); if let Some(head_text) = &head_text { - diff.update(head_text, &buffer); + git_diff.update(head_text, &buffer); } Self { @@ -432,7 +434,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, head_text, - diff, + git_diff, file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, @@ -447,6 +449,7 @@ impl Buffer { diagnostics_update_count: 0, diagnostics_timestamp: Default::default(), file_update_count: 0, + diff_update_count: 0, completion_triggers: Default::default(), completion_triggers_timestamp: Default::default(), deferred_ops: OperationQueue::new(), @@ -462,12 +465,13 @@ impl Buffer { BufferSnapshot { text, syntax, - git_hunks: self.diff.hunks(), + git_hunks: self.git_diff.hunks(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, + diff_update_count: self.diff_update_count, language: self.language.clone(), parse_count: self.parse_count, selections_update_count: self.selections_update_count, @@ -649,6 +653,14 @@ impl Buffer { task } + pub fn update_git(&mut self) { + if let Some(head_text) = &self.head_text { + let snapshot = self.snapshot(); + self.git_diff.update(head_text, &snapshot); + self.diff_update_count += 1; + } + } + pub fn close(&mut self, cx: &mut ModelContext) { cx.emit(Event::Closed); } @@ -673,6 +685,10 @@ impl Buffer { self.file_update_count } + pub fn diff_update_count(&self) -> usize { + self.diff_update_count + } + #[cfg(any(test, feature = "test-support"))] pub fn is_parsing(&self) -> bool { self.parsing_in_background @@ -2226,6 +2242,10 @@ impl BufferSnapshot { pub fn file_update_count(&self) -> usize { self.file_update_count } + + pub fn diff_update_count(&self) -> usize { + self.diff_update_count + } } pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { @@ -2260,6 +2280,7 @@ impl Clone for BufferSnapshot { selections_update_count: self.selections_update_count, diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, + diff_update_count: self.diff_update_count, language: self.language.clone(), parse_count: self.parse_count, } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3446dc0f0eaa07e9e9a30eb411f0e4871d2fedcc..ad3862c56ff2a60e7715f9858780b37c5e344fef 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -734,7 +734,7 @@ impl ItemHandle for ViewHandle { ); } - const GIT_DELAY: Duration = Duration::from_millis(800); + const GIT_DELAY: Duration = Duration::from_millis(600); let item = item.clone(); pending_git_update.fire_new( GIT_DELAY, From a86e93d46fa16b0c45acce1c3ba53ec728141157 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 9 Sep 2022 11:52:42 -0400 Subject: [PATCH 08/73] Checkpoint on incremental diff sumtree shenanigans --- crates/language/src/buffer.rs | 18 +-- crates/language/src/git.rs | 249 ++++++++++++++++++++++++++++------ crates/text/src/rope.rs | 7 + 3 files changed, 226 insertions(+), 48 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 10d7fa553510b5596ad68430285187f42d0a1dee..5159e316f945618bc6fe4aa771beec1c78a1f003 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -35,7 +35,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, vec, }; -use sum_tree::TreeMap; +use sum_tree::{SumTree, TreeMap}; use text::operation_queue::OperationQueue; pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *}; use theme::SyntaxTheme; @@ -48,7 +48,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, - head_text: Option, + head_text: Option, git_diff: BufferDiff, file: Option>, saved_version: clock::Global, @@ -77,7 +77,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub git_hunks: Arc<[DiffHunk]>, + pub git_hunks: SumTree>, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -371,7 +371,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(), + head_text: self.head_text.as_ref().map(|h| h.to_string()), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, } } @@ -421,10 +421,8 @@ impl Buffer { UNIX_EPOCH }; - let mut git_diff = BufferDiff::new(); - if let Some(head_text) = &head_text { - git_diff.update(head_text, &buffer); - } + let git_diff = BufferDiff::new(&head_text, &buffer); + let head_text = head_text.map(|h| Rope::from(h.as_str())); Self { saved_mtime, @@ -465,7 +463,7 @@ impl Buffer { BufferSnapshot { text, syntax, - git_hunks: self.git_diff.hunks(), + git_hunks: self.git_diff.hunks().clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -2175,6 +2173,8 @@ impl BufferSnapshot { &'a self, query_row_range: Range, ) -> impl 'a + Iterator> { + println!("{} hunks overall", self.git_hunks.iter().count()); + //This is pretty terrible, find a way to utilize sumtree traversal to accelerate this self.git_hunks.iter().filter_map(move |hunk| { let range = hunk.buffer_range.to_point(&self.text); diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 73e511ca482c6368c7559943df554a5d66031d2d..4a227c904dc8ffd6ce656f8bea73907bdcf06e08 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,8 +1,7 @@ use std::ops::Range; -use std::sync::Arc; -use sum_tree::Bias; -use text::{Anchor, Point}; +use sum_tree::{Bias, SumTree}; +use text::{Anchor, BufferSnapshot, Point, Rope}; pub use git2 as libgit; use libgit::{DiffOptions as GitOptions, Patch as GitPatch}; @@ -14,7 +13,7 @@ pub enum DiffHunkStatus { Removed, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DiffHunk { pub buffer_range: Range, pub head_range: Range, @@ -32,60 +31,232 @@ impl DiffHunk { } } -pub struct BufferDiff { - hunks: Arc<[DiffHunk]>, -} +impl sum_tree::Item for DiffHunk { + type Summary = DiffHunkSummary; -impl BufferDiff { - pub fn new() -> BufferDiff { - BufferDiff { - hunks: Arc::new([]), + fn summary(&self) -> Self::Summary { + DiffHunkSummary { + head_range: self.head_range.clone(), } } +} + +#[derive(Debug, Default, Clone)] +pub struct DiffHunkSummary { + head_range: Range, +} + +impl sum_tree::Summary for DiffHunkSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &Self::Context) { + self.head_range.start = self.head_range.start.min(other.head_range.start); + self.head_range.end = self.head_range.end.max(other.head_range.end); + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct HunkHeadEnd(usize); + +impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd { + fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &()) { + self.0 = summary.head_range.end; + } - pub fn hunks(&self) -> Arc<[DiffHunk]> { - self.hunks.clone() + fn from_summary(summary: &'a DiffHunkSummary, _: &()) -> Self { + HunkHeadEnd(summary.head_range.end) } +} - pub fn update(&mut self, head: &str, buffer: &text::BufferSnapshot) { - let head = head.as_bytes(); - let current = buffer.as_rope().to_string().into_bytes(); +struct HunkIter<'a> { + index: usize, + patch: GitPatch<'a>, +} +impl<'a> HunkIter<'a> { + fn diff(head: &'a [u8], current: &'a [u8]) -> Option { let mut options = GitOptions::default(); options.context_lines(0); - let patch = match GitPatch::from_buffers(head, None, ¤t, None, Some(&mut options)) { + let patch = match GitPatch::from_buffers(head, None, current, None, Some(&mut options)) { Ok(patch) => patch, - Err(_) => { - //Reset hunks in case of failure to avoid showing a stale (potentially erroneous) diff - self.hunks = Arc::new([]); - return; - } + Err(_) => return None, }; - let mut hunks = Vec::new(); - for index in 0..patch.num_hunks() { - let (hunk, _) = match patch.hunk(index) { - Ok(it) => it, - Err(_) => continue, - }; + Some(HunkIter { index: 0, patch }) + } + + fn next(&mut self, buffer: &BufferSnapshot) -> Option> { + if self.index >= self.patch.num_hunks() { + return None; + } + + let (hunk, _) = match self.patch.hunk(self.index) { + Ok(it) => it, + Err(_) => return None, + }; - let new_start = hunk.new_start() - 1; - let new_end = new_start + hunk.new_lines(); - let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); - let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); - let buffer_range = start_anchor..end_anchor; + let new_start = hunk.new_start() - 1; + let new_end = new_start + hunk.new_lines(); + let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); + let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); + let buffer_range = start_anchor..end_anchor; + //This is probably wrong? When does this trigger? Should buffer range also do this? + let head_range = if hunk.old_start() == 0 { + 0..0 + } else { let old_start = hunk.old_start() as usize - 1; let old_end = old_start + hunk.old_lines() as usize; - let head_range = old_start..old_end; + old_start..old_end + }; + + self.index += 1; + Some(DiffHunk { + buffer_range, + head_range, + }) + } +} + +pub struct BufferDiff { + last_update_version: clock::Global, + hunks: SumTree>, +} + +impl BufferDiff { + pub fn new(head_text: &Option, buffer: &text::BufferSnapshot) -> BufferDiff { + let hunks = if let Some(head_text) = head_text { + let buffer_string = buffer.as_rope().to_string(); + let buffer_bytes = buffer_string.as_bytes(); + let iter = HunkIter::diff(head_text.as_bytes(), buffer_bytes); + if let Some(mut iter) = iter { + println!("some iter"); + let mut hunks = SumTree::new(); + while let Some(hunk) = iter.next(buffer) { + println!("hunk"); + hunks.push(hunk, &()); + } + hunks + } else { + SumTree::new() + } + } else { + SumTree::new() + }; + + BufferDiff { + last_update_version: buffer.version().clone(), + hunks, + } + } + + pub fn hunks(&self) -> &SumTree> { + &self.hunks + } + + pub fn update(&mut self, head: &Rope, buffer: &text::BufferSnapshot) { + let expand_by = 20; + let combine_distance = 5; + + struct EditRange { + head_start: u32, + head_end: u32, + buffer_start: u32, + buffer_end: u32, + } + + let mut ranges = Vec::::new(); + + for edit in buffer.edits_since::(&self.last_update_version) { + //This bit is extremely wrong, this is not where these row lines should come from + let head_start = edit.old.start.row.saturating_sub(expand_by); + let head_end = (edit.old.end.row + expand_by).min(head.summary().lines.row + 1); + + let buffer_start = edit.new.start.row.saturating_sub(expand_by); + let buffer_end = (edit.new.end.row + expand_by).min(buffer.row_count()); - hunks.push(DiffHunk { - buffer_range, - head_range, - }); + if let Some(last_range) = ranges.last_mut() { + let head_distance = last_range.head_end.abs_diff(head_end); + let buffer_distance = last_range.buffer_end.abs_diff(buffer_end); + + if head_distance <= combine_distance || buffer_distance <= combine_distance { + last_range.head_start = last_range.head_start.min(head_start); + last_range.head_end = last_range.head_end.max(head_end); + + last_range.buffer_start = last_range.buffer_start.min(buffer_start); + last_range.buffer_end = last_range.buffer_end.max(buffer_end); + } else { + ranges.push(EditRange { + head_start, + head_end, + buffer_start, + buffer_end, + }); + } + } else { + ranges.push(EditRange { + head_start, + head_end, + buffer_start, + buffer_end, + }); + } + } + + self.last_update_version = buffer.version().clone(); + + let mut new_hunks = SumTree::new(); + let mut cursor = self.hunks.cursor::(); + + for range in ranges { + let head_range = range.head_start..range.head_end; + let head_slice = head.slice_rows(head_range.clone()); + let head_str = head_slice.to_string(); + + let buffer_range = range.buffer_start..range.buffer_end; + let buffer_slice = buffer.as_rope().slice_rows(buffer_range.clone()); + let buffer_str = buffer_slice.to_string(); + + println!("diffing head {:?}, buffer {:?}", head_range, buffer_range); + + let mut iter = match HunkIter::diff(head_str.as_bytes(), buffer_str.as_bytes()) { + Some(iter) => iter, + None => continue, + }; + + while let Some(hunk) = iter.next(buffer) { + println!("hunk"); + let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, &()); + println!("prefix len: {}", prefix.iter().count()); + new_hunks.extend(prefix.iter().cloned(), &()); + + new_hunks.push(hunk.clone(), &()); + + cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, &()); + println!("item: {:?}", cursor.item()); + if let Some(item) = cursor.item() { + if item.head_range.end <= hunk.head_range.end { + println!("skipping"); + cursor.next(&()); + } + } + } } - self.hunks = hunks.into(); + new_hunks.extend( + cursor + .suffix(&()) + .iter() + .map(|i| { + println!("extending with {i:?}"); + i + }) + .cloned(), + &(), + ); + drop(cursor); + + self.hunks = new_hunks; } } diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index d35ac46f45ec92c8c13f8c0cd90b3c08ca7c11bf..e148c048bbc57f8d8ef252cf8c001463822e6f46 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -54,6 +54,13 @@ impl Rope { cursor.slice(range.end) } + pub fn slice_rows(&self, range: Range) -> Rope { + //This would be more efficient with a forward advance after the first, but it's fine + let start = self.point_to_offset(Point::new(range.start, 0)); + let end = self.point_to_offset(Point::new(range.end, 0)); + self.slice(start..end) + } + pub fn push(&mut self, text: &str) { let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunk = ArrayString::new(); From 61ff24edc8f257e0ffbd691351653255cf2f0e66 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 9 Sep 2022 15:40:33 -0400 Subject: [PATCH 09/73] Move cloneable diff state into new snapshot type Co-Authored-By: Max Brunsfeld --- crates/language/src/buffer.rs | 31 ++----- crates/language/src/git.rs | 154 ++++++++++++++++++++++++++------ crates/sum_tree/src/sum_tree.rs | 6 ++ crates/text/src/anchor.rs | 2 +- 4 files changed, 140 insertions(+), 53 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5159e316f945618bc6fe4aa771beec1c78a1f003..37f21511333372bfea57a0102c807452d7be3dda 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,4 +1,4 @@ -use crate::git::{BufferDiff, DiffHunk}; +use crate::git::{BufferDiff, BufferDiffSnapshot, DiffHunk}; pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, @@ -35,7 +35,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, vec, }; -use sum_tree::{SumTree, TreeMap}; +use sum_tree::TreeMap; use text::operation_queue::OperationQueue; pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *}; use theme::SyntaxTheme; @@ -77,7 +77,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub git_hunks: SumTree>, + pub diff_snapshot: BufferDiffSnapshot, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -463,7 +463,7 @@ impl Buffer { BufferSnapshot { text, syntax, - git_hunks: self.git_diff.hunks().clone(), + diff_snapshot: self.git_diff.snapshot(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -2173,26 +2173,7 @@ impl BufferSnapshot { &'a self, query_row_range: Range, ) -> impl 'a + Iterator> { - println!("{} hunks overall", self.git_hunks.iter().count()); - //This is pretty terrible, find a way to utilize sumtree traversal to accelerate this - self.git_hunks.iter().filter_map(move |hunk| { - let range = hunk.buffer_range.to_point(&self.text); - - if range.start.row < query_row_range.end && query_row_range.start < range.end.row { - let end_row = if range.end.column > 0 { - range.end.row + 1 - } else { - range.end.row - }; - - Some(DiffHunk { - buffer_range: range.start.row..end_row, - head_range: hunk.head_range.clone(), - }) - } else { - None - } - }) + self.diff_snapshot.hunks_in_range(query_row_range, self) } pub fn diagnostics_in_range<'a, T, O>( @@ -2272,7 +2253,7 @@ impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { text: self.text.clone(), - git_hunks: self.git_hunks.clone(), + diff_snapshot: self.diff_snapshot.clone(), syntax: self.syntax.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 4a227c904dc8ffd6ce656f8bea73907bdcf06e08..09d74ad9f383a17c8adc49a042a1c14dd5a18a69 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,7 +1,7 @@ use std::ops::Range; use sum_tree::{Bias, SumTree}; -use text::{Anchor, BufferSnapshot, Point, Rope}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToPoint}; pub use git2 as libgit; use libgit::{DiffOptions as GitOptions, Patch as GitPatch}; @@ -13,10 +13,10 @@ pub enum DiffHunkStatus { Removed, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { pub buffer_range: Range, - pub head_range: Range, + pub head_range: Range, } impl DiffHunk { @@ -36,6 +36,7 @@ impl sum_tree::Item for DiffHunk { fn summary(&self) -> Self::Summary { DiffHunkSummary { + buffer_range: self.buffer_range.clone(), head_range: self.head_range.clone(), } } @@ -43,11 +44,12 @@ impl sum_tree::Item for DiffHunk { #[derive(Debug, Default, Clone)] pub struct DiffHunkSummary { - head_range: Range, + buffer_range: Range, + head_range: Range, } impl sum_tree::Summary for DiffHunkSummary { - type Context = (); + type Context = text::BufferSnapshot; fn add_summary(&mut self, other: &Self, _: &Self::Context) { self.head_range.start = self.head_range.start.min(other.head_range.start); @@ -55,19 +57,32 @@ impl sum_tree::Summary for DiffHunkSummary { } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct HunkHeadEnd(usize); +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct HunkHeadEnd(u32); impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd { - fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &()) { + fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) { self.0 = summary.head_range.end; } - fn from_summary(summary: &'a DiffHunkSummary, _: &()) -> Self { + fn from_summary(summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) -> Self { HunkHeadEnd(summary.head_range.end) } } +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct HunkBufferEnd(u32); + +impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { + fn add_summary(&mut self, summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) { + self.0 = summary.buffer_range.end.to_point(buffer).row; + } + + fn from_summary(summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) -> Self { + HunkBufferEnd(summary.buffer_range.end.to_point(buffer).row) + } +} + struct HunkIter<'a> { index: usize, patch: GitPatch<'a>, @@ -105,8 +120,8 @@ impl<'a> HunkIter<'a> { let head_range = if hunk.old_start() == 0 { 0..0 } else { - let old_start = hunk.old_start() as usize - 1; - let old_end = old_start + hunk.old_lines() as usize; + let old_start = hunk.old_start() - 1; + let old_end = old_start + hunk.old_lines(); old_start..old_end }; @@ -118,9 +133,48 @@ impl<'a> HunkIter<'a> { } } +#[derive(Clone)] +pub struct BufferDiffSnapshot { + tree: SumTree>, +} + +impl BufferDiffSnapshot { + pub fn hunks_in_range<'a>( + &'a self, + query_row_range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + println!("{} hunks overall", self.tree.iter().count()); + + self.tree.iter().filter_map(move |hunk| { + let range = hunk.buffer_range.to_point(&buffer); + + if range.start.row < query_row_range.end && query_row_range.start < range.end.row { + let end_row = if range.end.column > 0 { + range.end.row + 1 + } else { + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + head_range: hunk.head_range.clone(), + }) + } else { + None + } + }) + } + + #[cfg(test)] + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + self.hunks_in_range(0..u32::MAX, text) + } +} + pub struct BufferDiff { last_update_version: clock::Global, - hunks: SumTree>, + snapshot: BufferDiffSnapshot, } impl BufferDiff { @@ -128,13 +182,12 @@ impl BufferDiff { let hunks = if let Some(head_text) = head_text { let buffer_string = buffer.as_rope().to_string(); let buffer_bytes = buffer_string.as_bytes(); + let iter = HunkIter::diff(head_text.as_bytes(), buffer_bytes); if let Some(mut iter) = iter { - println!("some iter"); let mut hunks = SumTree::new(); while let Some(hunk) = iter.next(buffer) { - println!("hunk"); - hunks.push(hunk, &()); + hunks.push(hunk, buffer); } hunks } else { @@ -146,12 +199,12 @@ impl BufferDiff { BufferDiff { last_update_version: buffer.version().clone(), - hunks, + snapshot: BufferDiffSnapshot { tree: hunks }, } } - pub fn hunks(&self) -> &SumTree> { - &self.hunks + pub fn snapshot(&self) -> BufferDiffSnapshot { + self.snapshot.clone() } pub fn update(&mut self, head: &Rope, buffer: &text::BufferSnapshot) { @@ -206,7 +259,7 @@ impl BufferDiff { self.last_update_version = buffer.version().clone(); let mut new_hunks = SumTree::new(); - let mut cursor = self.hunks.cursor::(); + let mut cursor = self.snapshot.tree.cursor::(); for range in ranges { let head_range = range.head_start..range.head_end; @@ -226,18 +279,18 @@ impl BufferDiff { while let Some(hunk) = iter.next(buffer) { println!("hunk"); - let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, &()); + let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); println!("prefix len: {}", prefix.iter().count()); - new_hunks.extend(prefix.iter().cloned(), &()); + new_hunks.extend(prefix.iter().cloned(), buffer); - new_hunks.push(hunk.clone(), &()); + new_hunks.push(hunk.clone(), buffer); - cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, &()); + cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); println!("item: {:?}", cursor.item()); if let Some(item) = cursor.item() { if item.head_range.end <= hunk.head_range.end { println!("skipping"); - cursor.next(&()); + cursor.next(buffer); } } } @@ -245,18 +298,18 @@ impl BufferDiff { new_hunks.extend( cursor - .suffix(&()) + .suffix(buffer) .iter() .map(|i| { println!("extending with {i:?}"); i }) .cloned(), - &(), + buffer, ); drop(cursor); - self.hunks = new_hunks; + self.snapshot.tree = new_hunks; } } @@ -276,3 +329,50 @@ impl GitDiffEdit { } } } + +#[cfg(test)] +mod tests { + use super::*; + use text::Buffer; + use unindent::Unindent as _; + + #[gpui::test] + fn test_buffer_diff_simple() { + let head_text = " + one + two + three + " + .unindent(); + + let buffer_text = " + one + hello + three + " + .unindent(); + + let mut buffer = Buffer::new(0, 0, buffer_text); + let diff = BufferDiff::new(&Some(head_text.clone()), &buffer); + assert_eq!( + diff.snapshot.hunks(&buffer).collect::>(), + &[DiffHunk { + buffer_range: 1..2, + head_range: 1..2 + }] + ); + + buffer.edit([(0..0, "point five\n")]); + assert_eq!( + diff.snapshot.hunks(&buffer).collect::>(), + &[DiffHunk { + buffer_range: 2..3, + head_range: 1..2 + }] + ); + } + + // use rand::rngs::StdRng; + // #[gpui::test(iterations = 100)] + // fn test_buffer_diff_random(mut rng: StdRng) {} +} diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index cb05dff9673579bc51383cfaf67881560dbe0ef8..7beab3b7c59319a2cc6d44eea920070e953c4e7e 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -101,6 +101,12 @@ pub enum Bias { Right, } +impl Default for Bias { + fn default() -> Self { + Bias::Left + } +} + impl PartialOrd for Bias { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index dca95ce5d5ffaa8ab50cea80ca953160265c5e95..9f70ae1cc7b130c02f0edf01f06002e9f3b8dce1 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -4,7 +4,7 @@ use anyhow::Result; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] pub struct Anchor { pub timestamp: clock::Local, pub offset: usize, From a2e8fc79d9ff47686eb9b6442049f2f9331ba240 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 9 Sep 2022 17:32:19 -0400 Subject: [PATCH 10/73] Switch head range from row range to byte offset range Co-Authored-By: Max Brunsfeld --- crates/language/src/git.rs | 455 +++++++++++++++++++++++-------------- 1 file changed, 280 insertions(+), 175 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 09d74ad9f383a17c8adc49a042a1c14dd5a18a69..b3e2f7da51793e202a90f1ed564d962e6c6430fb 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -16,7 +16,7 @@ pub enum DiffHunkStatus { #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { pub buffer_range: Range, - pub head_range: Range, + pub head_range: Range, } impl DiffHunk { @@ -45,7 +45,7 @@ impl sum_tree::Item for DiffHunk { #[derive(Debug, Default, Clone)] pub struct DiffHunkSummary { buffer_range: Range, - head_range: Range, + head_range: Range, } impl sum_tree::Summary for DiffHunkSummary { @@ -58,7 +58,7 @@ impl sum_tree::Summary for DiffHunkSummary { } #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct HunkHeadEnd(u32); +struct HunkHeadEnd(usize); impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd { fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) { @@ -83,55 +83,63 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { } } -struct HunkIter<'a> { - index: usize, - patch: GitPatch<'a>, -} - -impl<'a> HunkIter<'a> { - fn diff(head: &'a [u8], current: &'a [u8]) -> Option { - let mut options = GitOptions::default(); - options.context_lines(0); - let patch = match GitPatch::from_buffers(head, None, current, None, Some(&mut options)) { - Ok(patch) => patch, - Err(_) => return None, - }; - - Some(HunkIter { index: 0, patch }) - } - - fn next(&mut self, buffer: &BufferSnapshot) -> Option> { - if self.index >= self.patch.num_hunks() { - return None; - } - - let (hunk, _) = match self.patch.hunk(self.index) { - Ok(it) => it, - Err(_) => return None, - }; - - let new_start = hunk.new_start() - 1; - let new_end = new_start + hunk.new_lines(); - let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); - let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); - let buffer_range = start_anchor..end_anchor; - - //This is probably wrong? When does this trigger? Should buffer range also do this? - let head_range = if hunk.old_start() == 0 { - 0..0 - } else { - let old_start = hunk.old_start() - 1; - let old_end = old_start + hunk.old_lines(); - old_start..old_end - }; - - self.index += 1; - Some(DiffHunk { - buffer_range, - head_range, - }) - } -} +// struct HunkIter<'a> { +// index: usize, +// patch: GitPatch<'a>, +// } + +// impl<'a> HunkIter<'a> { +// fn diff(head: &'a [u8], current: &'a [u8]) -> Option { +// let mut options = GitOptions::default(); +// options.context_lines(0); +// let patch = match GitPatch::from_buffers(head, None, current, None, Some(&mut options)) { +// Ok(patch) => patch, +// Err(_) => return None, +// }; + +// Some(HunkIter { index: 0, patch }) +// } + +// fn next(&mut self, buffer: &BufferSnapshot) -> Option> { +// if self.index >= self.patch.num_hunks() { +// return None; +// } + +// let (hunk, _) = match self.patch.hunk(self.index) { +// Ok(it) => it, +// Err(_) => return None, +// }; +// let hunk_line_count = self.patch.num_lines_in_hunk(self.index).unwrap(); + +// println!("{hunk:#?}"); +// for index in 0..hunk_line_count { +// println!("{:?}", self.patch.line_in_hunk(self.index, index)); +// } + +// let new_start = hunk.new_start() - 1; +// let new_end = new_start + hunk.new_lines(); +// let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); +// let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); +// let buffer_range = start_anchor..end_anchor; + +// //This is probably wrong? When does this trigger? Should buffer range also do this? +// let head_range = if hunk.old_start() == 0 { +// 0..0 +// } else { +// let old_start = hunk.old_start() - 1; +// let old_end = old_start + hunk.old_lines(); +// old_start..old_end +// }; + +// // let head_start_index = self.patch.line_in_hunk(self.index, 0) + +// self.index += 1; +// Some(DiffHunk { +// buffer_range, +// head_range, +// }) +// } +// } #[derive(Clone)] pub struct BufferDiffSnapshot { @@ -144,7 +152,7 @@ impl BufferDiffSnapshot { query_row_range: Range, buffer: &'a BufferSnapshot, ) -> impl 'a + Iterator> { - println!("{} hunks overall", self.tree.iter().count()); + // println!("{} hunks overall", self.tree.iter().count()); self.tree.iter().filter_map(move |hunk| { let range = hunk.buffer_range.to_point(&buffer); @@ -183,16 +191,100 @@ impl BufferDiff { let buffer_string = buffer.as_rope().to_string(); let buffer_bytes = buffer_string.as_bytes(); - let iter = HunkIter::diff(head_text.as_bytes(), buffer_bytes); - if let Some(mut iter) = iter { - let mut hunks = SumTree::new(); - while let Some(hunk) = iter.next(buffer) { - hunks.push(hunk, buffer); + let mut options = GitOptions::default(); + options.context_lines(0); + let patch = match GitPatch::from_buffers( + head_text.as_bytes(), + None, + buffer_bytes, + None, + Some(&mut options), + ) { + Ok(patch) => patch, + Err(_) => todo!("This needs to be handled"), + }; + + let mut hunks = SumTree::>::new(); + let mut delta = 0i64; + for i in 0..patch.num_hunks() { + let diff_line_item_count = patch.num_lines_in_hunk(i).unwrap(); + + // if diff_line_item_count == 0 { + // continue; + // } + + // let calc_line_diff_hunk = || { + + // }; + + // let first_line = patch.line_in_hunk(0).unwrap(); + // let mut hunk = + + for j in 0..diff_line_item_count { + let line = patch.line_in_hunk(i, j).unwrap(); + + let hunk = match line.origin_value() { + libgit::DiffLineType::Addition => { + let buffer_start = line.content_offset(); + let buffer_end = buffer_start as usize + line.content().len(); + let head_offset = (buffer_start - delta) as usize; + delta += line.content().len() as i64; + DiffHunk { + buffer_range: buffer.anchor_before(buffer_start as usize) + ..buffer.anchor_after(buffer_end), + head_range: head_offset..head_offset, + } + } + libgit::DiffLineType::Deletion => { + let head_start = line.content_offset(); + let head_end = head_start as usize + line.content().len(); + let buffer_offset = (head_start + delta) as usize; + delta -= line.content().len() as i64; + DiffHunk { + buffer_range: buffer.anchor_before(buffer_offset) + ..buffer.anchor_after(buffer_offset), + head_range: (head_start as usize)..head_end, + } + } + + libgit::DiffLineType::AddEOFNL => todo!(), + libgit::DiffLineType::ContextEOFNL => todo!(), + libgit::DiffLineType::DeleteEOFNL => todo!(), + libgit::DiffLineType::Context => unreachable!(), + libgit::DiffLineType::FileHeader => continue, + libgit::DiffLineType::HunkHeader => continue, + libgit::DiffLineType::Binary => continue, + }; + + let mut combined = false; + hunks.update_last( + |last_hunk| { + if last_hunk.head_range.end == hunk.head_range.start { + last_hunk.head_range.end = hunk.head_range.end; + last_hunk.buffer_range.end = hunk.buffer_range.end; + combined = true; + } + }, + buffer, + ); + if !combined { + hunks.push(hunk, buffer); + } } - hunks - } else { - SumTree::new() } + + // let iter = HunkIter::diff(head_text.as_bytes(), buffer_bytes); + // if let Some(mut iter) = iter { + // let mut hunks = SumTree::new(); + // while let Some(hunk) = iter.next(buffer) { + // hunks.push(hunk, buffer); + // } + // println!("========"); + // hunks + // } else { + // SumTree::new() + // } + hunks } else { SumTree::new() }; @@ -208,108 +300,108 @@ impl BufferDiff { } pub fn update(&mut self, head: &Rope, buffer: &text::BufferSnapshot) { - let expand_by = 20; - let combine_distance = 5; - - struct EditRange { - head_start: u32, - head_end: u32, - buffer_start: u32, - buffer_end: u32, - } - - let mut ranges = Vec::::new(); - - for edit in buffer.edits_since::(&self.last_update_version) { - //This bit is extremely wrong, this is not where these row lines should come from - let head_start = edit.old.start.row.saturating_sub(expand_by); - let head_end = (edit.old.end.row + expand_by).min(head.summary().lines.row + 1); - - let buffer_start = edit.new.start.row.saturating_sub(expand_by); - let buffer_end = (edit.new.end.row + expand_by).min(buffer.row_count()); - - if let Some(last_range) = ranges.last_mut() { - let head_distance = last_range.head_end.abs_diff(head_end); - let buffer_distance = last_range.buffer_end.abs_diff(buffer_end); - - if head_distance <= combine_distance || buffer_distance <= combine_distance { - last_range.head_start = last_range.head_start.min(head_start); - last_range.head_end = last_range.head_end.max(head_end); - - last_range.buffer_start = last_range.buffer_start.min(buffer_start); - last_range.buffer_end = last_range.buffer_end.max(buffer_end); - } else { - ranges.push(EditRange { - head_start, - head_end, - buffer_start, - buffer_end, - }); - } - } else { - ranges.push(EditRange { - head_start, - head_end, - buffer_start, - buffer_end, - }); - } - } - - self.last_update_version = buffer.version().clone(); - - let mut new_hunks = SumTree::new(); - let mut cursor = self.snapshot.tree.cursor::(); - - for range in ranges { - let head_range = range.head_start..range.head_end; - let head_slice = head.slice_rows(head_range.clone()); - let head_str = head_slice.to_string(); - - let buffer_range = range.buffer_start..range.buffer_end; - let buffer_slice = buffer.as_rope().slice_rows(buffer_range.clone()); - let buffer_str = buffer_slice.to_string(); - - println!("diffing head {:?}, buffer {:?}", head_range, buffer_range); - - let mut iter = match HunkIter::diff(head_str.as_bytes(), buffer_str.as_bytes()) { - Some(iter) => iter, - None => continue, - }; - - while let Some(hunk) = iter.next(buffer) { - println!("hunk"); - let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); - println!("prefix len: {}", prefix.iter().count()); - new_hunks.extend(prefix.iter().cloned(), buffer); - - new_hunks.push(hunk.clone(), buffer); - - cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); - println!("item: {:?}", cursor.item()); - if let Some(item) = cursor.item() { - if item.head_range.end <= hunk.head_range.end { - println!("skipping"); - cursor.next(buffer); - } - } - } - } - - new_hunks.extend( - cursor - .suffix(buffer) - .iter() - .map(|i| { - println!("extending with {i:?}"); - i - }) - .cloned(), - buffer, - ); - drop(cursor); - - self.snapshot.tree = new_hunks; + // let expand_by = 20; + // let combine_distance = 5; + + // struct EditRange { + // head_start: u32, + // head_end: u32, + // buffer_start: u32, + // buffer_end: u32, + // } + + // let mut ranges = Vec::::new(); + + // for edit in buffer.edits_since::(&self.last_update_version) { + // //This bit is extremely wrong, this is not where these row lines should come from + // let head_start = edit.old.start.row.saturating_sub(expand_by); + // let head_end = (edit.old.end.row + expand_by).min(head.summary().lines.row + 1); + + // let buffer_start = edit.new.start.row.saturating_sub(expand_by); + // let buffer_end = (edit.new.end.row + expand_by).min(buffer.row_count()); + + // if let Some(last_range) = ranges.last_mut() { + // let head_distance = last_range.head_end.abs_diff(head_end); + // let buffer_distance = last_range.buffer_end.abs_diff(buffer_end); + + // if head_distance <= combine_distance || buffer_distance <= combine_distance { + // last_range.head_start = last_range.head_start.min(head_start); + // last_range.head_end = last_range.head_end.max(head_end); + + // last_range.buffer_start = last_range.buffer_start.min(buffer_start); + // last_range.buffer_end = last_range.buffer_end.max(buffer_end); + // } else { + // ranges.push(EditRange { + // head_start, + // head_end, + // buffer_start, + // buffer_end, + // }); + // } + // } else { + // ranges.push(EditRange { + // head_start, + // head_end, + // buffer_start, + // buffer_end, + // }); + // } + // } + + // self.last_update_version = buffer.version().clone(); + + // let mut new_hunks = SumTree::new(); + // let mut cursor = self.snapshot.tree.cursor::(); + + // for range in ranges { + // let head_range = range.head_start..range.head_end; + // let head_slice = head.slice_rows(head_range.clone()); + // let head_str = head_slice.to_string(); + + // let buffer_range = range.buffer_start..range.buffer_end; + // let buffer_slice = buffer.as_rope().slice_rows(buffer_range.clone()); + // let buffer_str = buffer_slice.to_string(); + + // println!("diffing head {:?}, buffer {:?}", head_range, buffer_range); + + // let mut iter = match HunkIter::diff(head_str.as_bytes(), buffer_str.as_bytes()) { + // Some(iter) => iter, + // None => continue, + // }; + + // while let Some(hunk) = iter.next(buffer) { + // println!("hunk"); + // let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); + // println!("prefix len: {}", prefix.iter().count()); + // new_hunks.extend(prefix.iter().cloned(), buffer); + + // new_hunks.push(hunk.clone(), buffer); + + // cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); + // println!("item: {:?}", cursor.item()); + // if let Some(item) = cursor.item() { + // if item.head_range.end <= hunk.head_range.end { + // println!("skipping"); + // cursor.next(buffer); + // } + // } + // } + // } + + // new_hunks.extend( + // cursor + // .suffix(buffer) + // .iter() + // .map(|i| { + // println!("extending with {i:?}"); + // i + // }) + // .cloned(), + // buffer, + // ); + // drop(cursor); + + // self.snapshot.tree = new_hunks; } } @@ -354,22 +446,35 @@ mod tests { let mut buffer = Buffer::new(0, 0, buffer_text); let diff = BufferDiff::new(&Some(head_text.clone()), &buffer); - assert_eq!( - diff.snapshot.hunks(&buffer).collect::>(), - &[DiffHunk { - buffer_range: 1..2, - head_range: 1..2 - }] - ); + assert_hunks(&diff, &buffer, &head_text, &[(1..2, "two\n")]); buffer.edit([(0..0, "point five\n")]); + assert_hunks(&diff, &buffer, &head_text, &[(2..3, "two\n")]); + } + + #[track_caller] + fn assert_hunks( + diff: &BufferDiff, + buffer: &BufferSnapshot, + head_text: &str, + expected_hunks: &[(Range, &str)], + ) { + let hunks = diff.snapshot.hunks(buffer).collect::>(); assert_eq!( - diff.snapshot.hunks(&buffer).collect::>(), - &[DiffHunk { - buffer_range: 2..3, - head_range: 1..2 - }] + hunks.len(), + expected_hunks.len(), + "actual hunks are {hunks:#?}" ); + + let diff_iter = hunks.iter().enumerate(); + for ((index, hunk), (expected_range, expected_str)) in diff_iter.zip(expected_hunks) { + assert_eq!(&hunk.buffer_range, expected_range, "for hunk {index}"); + assert_eq!( + &head_text[hunk.head_range.clone()], + *expected_str, + "for hunk {index}" + ); + } } // use rand::rngs::StdRng; From 4b2040a7ca3d12f0614ed8ea9a5f0cb78b521343 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 12 Sep 2022 15:38:44 -0400 Subject: [PATCH 11/73] Move diff logic back into `BufferDiff::update` --- crates/language/src/buffer.rs | 3 +- crates/language/src/git.rs | 351 +++++++++------------------------- 2 files changed, 88 insertions(+), 266 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 37f21511333372bfea57a0102c807452d7be3dda..e75e17e54134cc92ffa120c3d5a2305c3cf617b3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -48,7 +48,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, - head_text: Option, + head_text: Option, git_diff: BufferDiff, file: Option>, saved_version: clock::Global, @@ -422,7 +422,6 @@ impl Buffer { }; let git_diff = BufferDiff::new(&head_text, &buffer); - let head_text = head_text.map(|h| Rope::from(h.as_str())); Self { saved_mtime, diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index b3e2f7da51793e202a90f1ed564d962e6c6430fb..4025a2a42fb404c08f5aed8e075dc2bc42689d59 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -83,64 +83,6 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { } } -// struct HunkIter<'a> { -// index: usize, -// patch: GitPatch<'a>, -// } - -// impl<'a> HunkIter<'a> { -// fn diff(head: &'a [u8], current: &'a [u8]) -> Option { -// let mut options = GitOptions::default(); -// options.context_lines(0); -// let patch = match GitPatch::from_buffers(head, None, current, None, Some(&mut options)) { -// Ok(patch) => patch, -// Err(_) => return None, -// }; - -// Some(HunkIter { index: 0, patch }) -// } - -// fn next(&mut self, buffer: &BufferSnapshot) -> Option> { -// if self.index >= self.patch.num_hunks() { -// return None; -// } - -// let (hunk, _) = match self.patch.hunk(self.index) { -// Ok(it) => it, -// Err(_) => return None, -// }; -// let hunk_line_count = self.patch.num_lines_in_hunk(self.index).unwrap(); - -// println!("{hunk:#?}"); -// for index in 0..hunk_line_count { -// println!("{:?}", self.patch.line_in_hunk(self.index, index)); -// } - -// let new_start = hunk.new_start() - 1; -// let new_end = new_start + hunk.new_lines(); -// let start_anchor = buffer.anchor_at(Point::new(new_start, 0), Bias::Left); -// let end_anchor = buffer.anchor_at(Point::new(new_end, 0), Bias::Left); -// let buffer_range = start_anchor..end_anchor; - -// //This is probably wrong? When does this trigger? Should buffer range also do this? -// let head_range = if hunk.old_start() == 0 { -// 0..0 -// } else { -// let old_start = hunk.old_start() - 1; -// let old_end = old_start + hunk.old_lines(); -// old_start..old_end -// }; - -// // let head_start_index = self.patch.line_in_hunk(self.index, 0) - -// self.index += 1; -// Some(DiffHunk { -// buffer_range, -// head_range, -// }) -// } -// } - #[derive(Clone)] pub struct BufferDiffSnapshot { tree: SumTree>, @@ -187,221 +129,102 @@ pub struct BufferDiff { impl BufferDiff { pub fn new(head_text: &Option, buffer: &text::BufferSnapshot) -> BufferDiff { - let hunks = if let Some(head_text) = head_text { - let buffer_string = buffer.as_rope().to_string(); - let buffer_bytes = buffer_string.as_bytes(); - - let mut options = GitOptions::default(); - options.context_lines(0); - let patch = match GitPatch::from_buffers( - head_text.as_bytes(), - None, - buffer_bytes, - None, - Some(&mut options), - ) { - Ok(patch) => patch, - Err(_) => todo!("This needs to be handled"), - }; - - let mut hunks = SumTree::>::new(); - let mut delta = 0i64; - for i in 0..patch.num_hunks() { - let diff_line_item_count = patch.num_lines_in_hunk(i).unwrap(); - - // if diff_line_item_count == 0 { - // continue; - // } - - // let calc_line_diff_hunk = || { - - // }; - - // let first_line = patch.line_in_hunk(0).unwrap(); - // let mut hunk = - - for j in 0..diff_line_item_count { - let line = patch.line_in_hunk(i, j).unwrap(); - - let hunk = match line.origin_value() { - libgit::DiffLineType::Addition => { - let buffer_start = line.content_offset(); - let buffer_end = buffer_start as usize + line.content().len(); - let head_offset = (buffer_start - delta) as usize; - delta += line.content().len() as i64; - DiffHunk { - buffer_range: buffer.anchor_before(buffer_start as usize) - ..buffer.anchor_after(buffer_end), - head_range: head_offset..head_offset, - } - } - libgit::DiffLineType::Deletion => { - let head_start = line.content_offset(); - let head_end = head_start as usize + line.content().len(); - let buffer_offset = (head_start + delta) as usize; - delta -= line.content().len() as i64; - DiffHunk { - buffer_range: buffer.anchor_before(buffer_offset) - ..buffer.anchor_after(buffer_offset), - head_range: (head_start as usize)..head_end, - } - } - - libgit::DiffLineType::AddEOFNL => todo!(), - libgit::DiffLineType::ContextEOFNL => todo!(), - libgit::DiffLineType::DeleteEOFNL => todo!(), - libgit::DiffLineType::Context => unreachable!(), - libgit::DiffLineType::FileHeader => continue, - libgit::DiffLineType::HunkHeader => continue, - libgit::DiffLineType::Binary => continue, - }; - - let mut combined = false; - hunks.update_last( - |last_hunk| { - if last_hunk.head_range.end == hunk.head_range.start { - last_hunk.head_range.end = hunk.head_range.end; - last_hunk.buffer_range.end = hunk.buffer_range.end; - combined = true; - } - }, - buffer, - ); - if !combined { - hunks.push(hunk, buffer); - } - } - } - - // let iter = HunkIter::diff(head_text.as_bytes(), buffer_bytes); - // if let Some(mut iter) = iter { - // let mut hunks = SumTree::new(); - // while let Some(hunk) = iter.next(buffer) { - // hunks.push(hunk, buffer); - // } - // println!("========"); - // hunks - // } else { - // SumTree::new() - // } - hunks - } else { - SumTree::new() + let mut instance = BufferDiff { + last_update_version: buffer.version().clone(), + snapshot: BufferDiffSnapshot { + tree: SumTree::new(), + }, }; - BufferDiff { - last_update_version: buffer.version().clone(), - snapshot: BufferDiffSnapshot { tree: hunks }, + if let Some(head_text) = head_text { + instance.update(head_text, buffer); } + + instance } pub fn snapshot(&self) -> BufferDiffSnapshot { self.snapshot.clone() } - pub fn update(&mut self, head: &Rope, buffer: &text::BufferSnapshot) { - // let expand_by = 20; - // let combine_distance = 5; - - // struct EditRange { - // head_start: u32, - // head_end: u32, - // buffer_start: u32, - // buffer_end: u32, - // } - - // let mut ranges = Vec::::new(); - - // for edit in buffer.edits_since::(&self.last_update_version) { - // //This bit is extremely wrong, this is not where these row lines should come from - // let head_start = edit.old.start.row.saturating_sub(expand_by); - // let head_end = (edit.old.end.row + expand_by).min(head.summary().lines.row + 1); - - // let buffer_start = edit.new.start.row.saturating_sub(expand_by); - // let buffer_end = (edit.new.end.row + expand_by).min(buffer.row_count()); - - // if let Some(last_range) = ranges.last_mut() { - // let head_distance = last_range.head_end.abs_diff(head_end); - // let buffer_distance = last_range.buffer_end.abs_diff(buffer_end); - - // if head_distance <= combine_distance || buffer_distance <= combine_distance { - // last_range.head_start = last_range.head_start.min(head_start); - // last_range.head_end = last_range.head_end.max(head_end); - - // last_range.buffer_start = last_range.buffer_start.min(buffer_start); - // last_range.buffer_end = last_range.buffer_end.max(buffer_end); - // } else { - // ranges.push(EditRange { - // head_start, - // head_end, - // buffer_start, - // buffer_end, - // }); - // } - // } else { - // ranges.push(EditRange { - // head_start, - // head_end, - // buffer_start, - // buffer_end, - // }); - // } - // } - - // self.last_update_version = buffer.version().clone(); - - // let mut new_hunks = SumTree::new(); - // let mut cursor = self.snapshot.tree.cursor::(); - - // for range in ranges { - // let head_range = range.head_start..range.head_end; - // let head_slice = head.slice_rows(head_range.clone()); - // let head_str = head_slice.to_string(); - - // let buffer_range = range.buffer_start..range.buffer_end; - // let buffer_slice = buffer.as_rope().slice_rows(buffer_range.clone()); - // let buffer_str = buffer_slice.to_string(); - - // println!("diffing head {:?}, buffer {:?}", head_range, buffer_range); - - // let mut iter = match HunkIter::diff(head_str.as_bytes(), buffer_str.as_bytes()) { - // Some(iter) => iter, - // None => continue, - // }; - - // while let Some(hunk) = iter.next(buffer) { - // println!("hunk"); - // let prefix = cursor.slice(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); - // println!("prefix len: {}", prefix.iter().count()); - // new_hunks.extend(prefix.iter().cloned(), buffer); - - // new_hunks.push(hunk.clone(), buffer); - - // cursor.seek(&HunkHeadEnd(hunk.head_range.end), Bias::Right, buffer); - // println!("item: {:?}", cursor.item()); - // if let Some(item) = cursor.item() { - // if item.head_range.end <= hunk.head_range.end { - // println!("skipping"); - // cursor.next(buffer); - // } - // } - // } - // } - - // new_hunks.extend( - // cursor - // .suffix(buffer) - // .iter() - // .map(|i| { - // println!("extending with {i:?}"); - // i - // }) - // .cloned(), - // buffer, - // ); - // drop(cursor); - - // self.snapshot.tree = new_hunks; + pub fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { + let buffer_string = buffer.as_rope().to_string(); + let buffer_bytes = buffer_string.as_bytes(); + + let mut options = GitOptions::default(); + options.context_lines(0); + let patch = match GitPatch::from_buffers( + head_text.as_bytes(), + None, + buffer_bytes, + None, + Some(&mut options), + ) { + Ok(patch) => patch, + Err(_) => todo!("This needs to be handled"), + }; + + let mut hunks = SumTree::>::new(); + let mut delta = 0i64; + for hunk_index in 0..patch.num_hunks() { + for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { + let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + + let hunk = match line.origin_value() { + libgit::DiffLineType::Addition => { + let buffer_start = line.content_offset(); + let buffer_end = buffer_start as usize + line.content().len(); + let head_offset = (buffer_start - delta) as usize; + delta += line.content().len() as i64; + DiffHunk { + buffer_range: buffer.anchor_before(buffer_start as usize) + ..buffer.anchor_after(buffer_end), + head_range: head_offset..head_offset, + } + } + + libgit::DiffLineType::Deletion => { + let head_start = line.content_offset(); + let head_end = head_start as usize + line.content().len(); + let buffer_offset = (head_start + delta) as usize; + delta -= line.content().len() as i64; + DiffHunk { + buffer_range: buffer.anchor_before(buffer_offset) + ..buffer.anchor_after(buffer_offset), + head_range: (head_start as usize)..head_end, + } + } + + libgit::DiffLineType::AddEOFNL => todo!(), + libgit::DiffLineType::ContextEOFNL => todo!(), + libgit::DiffLineType::DeleteEOFNL => todo!(), + + libgit::DiffLineType::FileHeader => continue, + libgit::DiffLineType::HunkHeader => continue, + libgit::DiffLineType::Binary => continue, + + //We specifically tell git to not give us context lines + libgit::DiffLineType::Context => unreachable!(), + }; + + let mut combined = false; + hunks.update_last( + |last_hunk| { + if last_hunk.head_range.end == hunk.head_range.start { + last_hunk.head_range.end = hunk.head_range.end; + last_hunk.buffer_range.end = hunk.buffer_range.end; + combined = true; + } + }, + buffer, + ); + if !combined { + hunks.push(hunk, buffer); + } + } + } + + self.snapshot.tree = hunks; } } From e0ea932fa7d345ddfee557cd29b577b98d515f31 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 13 Sep 2022 20:41:38 -0400 Subject: [PATCH 12/73] Checkpoint preparing for a more organized approach to incremental diff --- crates/language/src/git.rs | 392 ++++++++++++++++++++++++++++--------- 1 file changed, 298 insertions(+), 94 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 4025a2a42fb404c08f5aed8e075dc2bc42689d59..642ed5f29721f6a2ec6dd783fa1a693b18c7b6a6 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,10 +1,14 @@ use std::ops::Range; +use client::proto::create_buffer_for_peer; use sum_tree::{Bias, SumTree}; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToPoint}; pub use git2 as libgit; -use libgit::{DiffOptions as GitOptions, Patch as GitPatch}; +use libgit::{ + DiffLine as GitDiffLine, DiffLineType as GitDiffLineType, DiffOptions as GitOptions, + Patch as GitPatch, +}; #[derive(Debug, Clone, Copy)] pub enum DiffHunkStatus { @@ -16,12 +20,12 @@ pub enum DiffHunkStatus { #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { pub buffer_range: Range, - pub head_range: Range, + pub head_byte_range: Range, } impl DiffHunk { pub fn status(&self) -> DiffHunkStatus { - if self.head_range.is_empty() { + if self.head_byte_range.is_empty() { DiffHunkStatus::Added } else if self.buffer_range.is_empty() { DiffHunkStatus::Removed @@ -37,7 +41,7 @@ impl sum_tree::Item for DiffHunk { fn summary(&self) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), - head_range: self.head_range.clone(), + head_range: self.head_byte_range.clone(), } } } @@ -70,6 +74,19 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd { } } +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct HunkBufferStart(u32); + +impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferStart { + fn add_summary(&mut self, summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) { + self.0 = summary.buffer_range.start.to_point(buffer).row; + } + + fn from_summary(summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) -> Self { + HunkBufferStart(summary.buffer_range.start.to_point(buffer).row) + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] struct HunkBufferEnd(u32); @@ -83,6 +100,40 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { } } +struct HunkLineIter<'a, 'b> { + patch: &'a GitPatch<'b>, + hunk_index: usize, + line_index: usize, +} + +impl<'a, 'b> HunkLineIter<'a, 'b> { + fn new(patch: &'a GitPatch<'b>, hunk_index: usize) -> Self { + HunkLineIter { + patch, + hunk_index, + line_index: 0, + } + } +} + +impl<'a, 'b> std::iter::Iterator for HunkLineIter<'a, 'b> { + type Item = GitDiffLine<'b>; + + fn next(&mut self) -> Option { + if self.line_index >= self.patch.num_lines_in_hunk(self.hunk_index).unwrap() { + return None; + } + + let line_index = self.line_index; + self.line_index += 1; + Some( + self.patch + .line_in_hunk(self.hunk_index, line_index) + .unwrap(), + ) + } +} + #[derive(Clone)] pub struct BufferDiffSnapshot { tree: SumTree>, @@ -94,8 +145,6 @@ impl BufferDiffSnapshot { query_row_range: Range, buffer: &'a BufferSnapshot, ) -> impl 'a + Iterator> { - // println!("{} hunks overall", self.tree.iter().count()); - self.tree.iter().filter_map(move |hunk| { let range = hunk.buffer_range.to_point(&buffer); @@ -108,7 +157,7 @@ impl BufferDiffSnapshot { Some(DiffHunk { buffer_range: range.start.row..end_row, - head_range: hunk.head_range.clone(), + head_byte_range: hunk.head_byte_range.clone(), }) } else { None @@ -129,18 +178,32 @@ pub struct BufferDiff { impl BufferDiff { pub fn new(head_text: &Option, buffer: &text::BufferSnapshot) -> BufferDiff { - let mut instance = BufferDiff { - last_update_version: buffer.version().clone(), - snapshot: BufferDiffSnapshot { - tree: SumTree::new(), - }, - }; + let mut tree = SumTree::new(); if let Some(head_text) = head_text { - instance.update(head_text, buffer); + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(&head_text, &buffer_text); + + if let Some(patch) = patch { + let mut buffer_divergence = 0; + + for hunk_index in 0..patch.num_hunks() { + let patch = Self::process_patch_hunk( + &mut buffer_divergence, + &patch, + hunk_index, + buffer, + ); + + tree.push(patch, buffer); + } + } } - instance + BufferDiff { + last_update_version: buffer.version().clone(), + snapshot: BufferDiffSnapshot { tree }, + } } pub fn snapshot(&self) -> BufferDiffSnapshot { @@ -148,101 +211,242 @@ impl BufferDiff { } pub fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { - let buffer_string = buffer.as_rope().to_string(); - let buffer_bytes = buffer_string.as_bytes(); + // let buffer_string = buffer.as_rope().to_string(); + // let buffer_bytes = buffer_string.as_bytes(); + + // let mut options = GitOptions::default(); + // options.context_lines(0); + // let patch = match GitPatch::from_buffers( + // head_text.as_bytes(), + // None, + // buffer_bytes, + // None, + // Some(&mut options), + // ) { + // Ok(patch) => patch, + // Err(_) => todo!("This needs to be handled"), + // }; + + // let mut hunks = SumTree::>::new(); + // let mut delta = 0i64; + // for hunk_index in 0..patch.num_hunks() { + // for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { + // let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + + // let hunk = match line.origin_value() { + // GitDiffLineType::Addition => { + // let buffer_start = line.content_offset(); + // let buffer_end = buffer_start as usize + line.content().len(); + // let head_offset = (buffer_start - delta) as usize; + // delta += line.content().len() as i64; + // DiffHunk { + // buffer_range: buffer.anchor_before(buffer_start as usize) + // ..buffer.anchor_after(buffer_end), + // head_byte_range: head_offset..head_offset, + // } + // } + + // GitDiffLineType::Deletion => { + // let head_start = line.content_offset(); + // let head_end = head_start as usize + line.content().len(); + // let buffer_offset = (head_start + delta) as usize; + // delta -= line.content().len() as i64; + // DiffHunk { + // buffer_range: buffer.anchor_before(buffer_offset) + // ..buffer.anchor_after(buffer_offset), + // head_byte_range: (head_start as usize)..head_end, + // } + // } + + // _ => continue, + // }; + + // let mut combined = false; + // hunks.update_last( + // |last_hunk| { + // if last_hunk.head_byte_range.end == hunk.head_byte_range.start { + // last_hunk.head_byte_range.end = hunk.head_byte_range.end; + // last_hunk.buffer_range.end = hunk.buffer_range.end; + // combined = true; + // } + // }, + // buffer, + // ); + // if !combined { + // hunks.push(hunk, buffer); + // } + // } + // } + + // println!("====="); + // for hunk in hunks.iter() { + // let buffer_range = hunk.buffer_range.to_point(&buffer); + // println!( + // "hunk in buffer range {buffer_range:?}, head slice {:?}", + // &head_text[hunk.head_byte_range.clone()] + // ); + // } + // println!("====="); + + // self.snapshot.tree = hunks; + } + pub fn actual_update( + &mut self, + head_text: &str, + buffer: &BufferSnapshot, + ) -> Option> { + for edit_range in self.group_edit_ranges(buffer) { + // let patch = self.diff(head, current)?; + } + + None + } + + fn diff<'a>(head: &'a str, current: &'a str) -> Option> { let mut options = GitOptions::default(); options.context_lines(0); - let patch = match GitPatch::from_buffers( - head_text.as_bytes(), + + let patch = GitPatch::from_buffers( + head.as_bytes(), None, - buffer_bytes, + current.as_bytes(), None, Some(&mut options), - ) { - Ok(patch) => patch, - Err(_) => todo!("This needs to be handled"), - }; - - let mut hunks = SumTree::>::new(); - let mut delta = 0i64; - for hunk_index in 0..patch.num_hunks() { - for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { - let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); - - let hunk = match line.origin_value() { - libgit::DiffLineType::Addition => { - let buffer_start = line.content_offset(); - let buffer_end = buffer_start as usize + line.content().len(); - let head_offset = (buffer_start - delta) as usize; - delta += line.content().len() as i64; - DiffHunk { - buffer_range: buffer.anchor_before(buffer_start as usize) - ..buffer.anchor_after(buffer_end), - head_range: head_offset..head_offset, - } - } - - libgit::DiffLineType::Deletion => { - let head_start = line.content_offset(); - let head_end = head_start as usize + line.content().len(); - let buffer_offset = (head_start + delta) as usize; - delta -= line.content().len() as i64; - DiffHunk { - buffer_range: buffer.anchor_before(buffer_offset) - ..buffer.anchor_after(buffer_offset), - head_range: (head_start as usize)..head_end, - } - } - - libgit::DiffLineType::AddEOFNL => todo!(), - libgit::DiffLineType::ContextEOFNL => todo!(), - libgit::DiffLineType::DeleteEOFNL => todo!(), - - libgit::DiffLineType::FileHeader => continue, - libgit::DiffLineType::HunkHeader => continue, - libgit::DiffLineType::Binary => continue, - - //We specifically tell git to not give us context lines - libgit::DiffLineType::Context => unreachable!(), - }; + ); + + match patch { + Ok(patch) => Some(patch), - let mut combined = false; - hunks.update_last( - |last_hunk| { - if last_hunk.head_range.end == hunk.head_range.start { - last_hunk.head_range.end = hunk.head_range.end; - last_hunk.buffer_range.end = hunk.buffer_range.end; - combined = true; - } - }, - buffer, - ); - if !combined { - hunks.push(hunk, buffer); + Err(err) => { + log::error!("`GitPatch::from_buffers` failed: {}", err); + None + } + } + } + + fn group_edit_ranges(&mut self, buffer: &text::BufferSnapshot) -> Vec> { + const EXPAND_BY: u32 = 20; + const COMBINE_DISTANCE: u32 = 5; + + // let mut cursor = self.snapshot.tree.cursor::(); + + let mut ranges = Vec::>::new(); + + for edit in buffer.edits_since::(&self.last_update_version) { + let buffer_start = edit.new.start.row.saturating_sub(EXPAND_BY); + let buffer_end = (edit.new.end.row + EXPAND_BY).min(buffer.row_count()); + + match ranges.last_mut() { + Some(last_range) if last_range.end.abs_diff(buffer_end) <= COMBINE_DISTANCE => { + last_range.start = last_range.start.min(buffer_start); + last_range.end = last_range.end.max(buffer_end); } + + _ => ranges.push(buffer_start..buffer_end), } } - self.snapshot.tree = hunks; + self.last_update_version = buffer.version().clone(); + ranges } -} -#[derive(Debug, Clone, Copy)] -pub enum GitDiffEdit { - Added(u32), - Modified(u32), - Removed(u32), -} + fn process_patch_hunk<'a>( + buffer_divergence: &mut isize, + patch: &GitPatch<'a>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + ) -> DiffHunk { + let mut buffer_byte_range: Option> = None; + let mut head_byte_range: Option> = None; + + for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { + let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + let kind = line.origin_value(); + println!("line index: {line_index}, kind: {kind:?}"); + let content_offset = line.content_offset() as isize; + + match (kind, &mut buffer_byte_range, &mut head_byte_range) { + (GitDiffLineType::Addition, None, _) => { + let start = *buffer_divergence + content_offset; + let end = start + line.content().len() as isize; + buffer_byte_range = Some(start as usize..end as usize); + } + + (GitDiffLineType::Addition, Some(buffer_byte_range), _) => { + buffer_byte_range.end = content_offset as usize; + } + + (GitDiffLineType::Deletion, _, None) => { + let end = content_offset + line.content().len() as isize; + head_byte_range = Some(content_offset as usize..end as usize); + } -impl GitDiffEdit { - pub fn line(self) -> u32 { - use GitDiffEdit::*; + (GitDiffLineType::Deletion, _, Some(head_byte_range)) => { + let end = content_offset + line.content().len() as isize; + head_byte_range.end = end as usize; + } + + _ => {} + } + } - match self { - Added(line) | Modified(line) | Removed(line) => line, + //unwrap_or deletion without addition + let buffer_byte_range = buffer_byte_range.unwrap_or(0..0); + //unwrap_or addition without deletion + let head_byte_range = head_byte_range.unwrap_or(0..0); + + *buffer_divergence += buffer_byte_range.len() as isize - head_byte_range.len() as isize; + + DiffHunk { + buffer_range: buffer.anchor_before(buffer_byte_range.start) + ..buffer.anchor_before(buffer_byte_range.end), + head_byte_range, } } + + fn name() { + // if self.hunk_index >= self.patch.num_hunks() { + // return None; + // } + + // let mut line_iter = HunkLineIter::new(&self.patch, self.hunk_index); + // let line = line_iter.find(|line| { + // matches!( + // line.origin_value(), + // GitDiffLineType::Addition | GitDiffLineType::Deletion + // ) + // })?; + + // //For the first line of a hunk the content offset is equally valid for an addition or deletion + // let content_offset = line.content_offset() as usize; + + // let mut buffer_range = content_offset..content_offset; + // let mut head_byte_range = match line.origin_value() { + // GitDiffLineType::Addition => content_offset..content_offset, + // GitDiffLineType::Deletion => content_offset..content_offset + line.content().len(), + // _ => unreachable!(), + // }; + + // for line in line_iter { + // match line.origin_value() { + // GitDiffLineType::Addition => { + // // buffer_range.end = + // } + + // GitDiffLineType::Deletion => {} + + // _ => continue, + // } + // } + + // self.hunk_index += 1; + // Some(DiffHunk { + // buffer_range: buffer.anchor_before(buffer_range.start) + // ..buffer.anchor_before(buffer_range.end), + // head_byte_range, + // }) + } } #[cfg(test)] @@ -293,7 +497,7 @@ mod tests { for ((index, hunk), (expected_range, expected_str)) in diff_iter.zip(expected_hunks) { assert_eq!(&hunk.buffer_range, expected_range, "for hunk {index}"); assert_eq!( - &head_text[hunk.head_range.clone()], + &head_text[hunk.head_byte_range.clone()], *expected_str, "for hunk {index}" ); From 2f7283fd13e111452dd63b3c3bb5599ff90a2d5e Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 14 Sep 2022 11:18:33 -0400 Subject: [PATCH 13/73] buffer_divergence doesn't seem to be a concept that needs to be tracked --- crates/language/src/git.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 642ed5f29721f6a2ec6dd783fa1a693b18c7b6a6..2ac0400da85165b84b8213c8fba97591eb15aeb5 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -185,16 +185,8 @@ impl BufferDiff { let patch = Self::diff(&head_text, &buffer_text); if let Some(patch) = patch { - let mut buffer_divergence = 0; - for hunk_index in 0..patch.num_hunks() { - let patch = Self::process_patch_hunk( - &mut buffer_divergence, - &patch, - hunk_index, - buffer, - ); - + let patch = Self::process_patch_hunk(&patch, hunk_index, buffer); tree.push(patch, buffer); } } @@ -352,7 +344,6 @@ impl BufferDiff { } fn process_patch_hunk<'a>( - buffer_divergence: &mut isize, patch: &GitPatch<'a>, hunk_index: usize, buffer: &text::BufferSnapshot, @@ -363,18 +354,17 @@ impl BufferDiff { for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); let kind = line.origin_value(); - println!("line index: {line_index}, kind: {kind:?}"); let content_offset = line.content_offset() as isize; match (kind, &mut buffer_byte_range, &mut head_byte_range) { (GitDiffLineType::Addition, None, _) => { - let start = *buffer_divergence + content_offset; - let end = start + line.content().len() as isize; - buffer_byte_range = Some(start as usize..end as usize); + let end = content_offset + line.content().len() as isize; + buffer_byte_range = Some(content_offset as usize..end as usize); } (GitDiffLineType::Addition, Some(buffer_byte_range), _) => { - buffer_byte_range.end = content_offset as usize; + let end = content_offset + line.content().len() as isize; + buffer_byte_range.end = end as usize; } (GitDiffLineType::Deletion, _, None) => { @@ -396,8 +386,6 @@ impl BufferDiff { //unwrap_or addition without deletion let head_byte_range = head_byte_range.unwrap_or(0..0); - *buffer_divergence += buffer_byte_range.len() as isize - head_byte_range.len() as isize; - DiffHunk { buffer_range: buffer.anchor_before(buffer_byte_range.start) ..buffer.anchor_before(buffer_byte_range.end), From 96917a8007018c08a373a8fc09608abc73cb3c1d Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 14 Sep 2022 11:20:01 -0400 Subject: [PATCH 14/73] Small clean --- crates/language/src/git.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 2ac0400da85165b84b8213c8fba97591eb15aeb5..361a44f3778c05ccaa5d44631f0a1a68ac489aaf 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -355,25 +355,26 @@ impl BufferDiff { let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); let kind = line.origin_value(); let content_offset = line.content_offset() as isize; + let content_len = line.content().len() as isize; match (kind, &mut buffer_byte_range, &mut head_byte_range) { (GitDiffLineType::Addition, None, _) => { - let end = content_offset + line.content().len() as isize; + let end = content_offset + content_len; buffer_byte_range = Some(content_offset as usize..end as usize); } (GitDiffLineType::Addition, Some(buffer_byte_range), _) => { - let end = content_offset + line.content().len() as isize; + let end = content_offset + content_len; buffer_byte_range.end = end as usize; } (GitDiffLineType::Deletion, _, None) => { - let end = content_offset + line.content().len() as isize; + let end = content_offset + content_len; head_byte_range = Some(content_offset as usize..end as usize); } (GitDiffLineType::Deletion, _, Some(head_byte_range)) => { - let end = content_offset + line.content().len() as isize; + let end = content_offset + content_len; head_byte_range.end = end as usize; } From c1249a3d84195869cbff0cfecaeff4ab8b0b2a52 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 14 Sep 2022 12:38:23 -0400 Subject: [PATCH 15/73] Handle deletions more robustly and correctly --- crates/editor/src/element.rs | 4 ++-- crates/language/src/git.rs | 31 ++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4ee14407b893909d46e1d0894a51c40f9e17d78e..3a5166e17e9be115174ec371be6b6baab3925c9d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -552,10 +552,10 @@ impl EditorElement { //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { - let row_above = hunk.buffer_range.start; + let row = hunk.buffer_range.start as i64 - 1; let offset = line_height / 2.; - let start_y = row_above as f32 * line_height + offset - scroll_top; + let start_y = row as f32 * line_height + offset - scroll_top; let end_y = start_y + line_height; let width = 0.4 * line_height; diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 361a44f3778c05ccaa5d44631f0a1a68ac489aaf..f2adee42fab42f98ae1374e6033bc9fe3e075fd7 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,8 +1,7 @@ use std::ops::Range; -use client::proto::create_buffer_for_peer; use sum_tree::{Bias, SumTree}; -use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToPoint}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; pub use git2 as libgit; use libgit::{ @@ -148,7 +147,7 @@ impl BufferDiffSnapshot { self.tree.iter().filter_map(move |hunk| { let range = hunk.buffer_range.to_point(&buffer); - if range.start.row < query_row_range.end && query_row_range.start < range.end.row { + if range.start.row <= query_row_range.end && query_row_range.start <= range.end.row { let end_row = if range.end.column > 0 { range.end.row + 1 } else { @@ -186,8 +185,8 @@ impl BufferDiff { if let Some(patch) = patch { for hunk_index in 0..patch.num_hunks() { - let patch = Self::process_patch_hunk(&patch, hunk_index, buffer); - tree.push(patch, buffer); + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer); + tree.push(hunk, buffer); } } } @@ -348,10 +347,14 @@ impl BufferDiff { hunk_index: usize, buffer: &text::BufferSnapshot, ) -> DiffHunk { + let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); + assert!(line_item_count > 0); + + let mut first_deletion_buffer_row: Option = None; let mut buffer_byte_range: Option> = None; let mut head_byte_range: Option> = None; - for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { + for line_index in 0..line_item_count { let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); let kind = line.origin_value(); let content_offset = line.content_offset() as isize; @@ -380,10 +383,24 @@ impl BufferDiff { _ => {} } + + if kind == GitDiffLineType::Deletion && first_deletion_buffer_row.is_none() { + //old_lineno is guarenteed to be Some for deletions + //libgit gives us line numbers that are 1-indexed but also returns a 0 for some states + let row = line.old_lineno().unwrap().saturating_sub(1); + first_deletion_buffer_row = Some(row); + } } //unwrap_or deletion without addition - let buffer_byte_range = buffer_byte_range.unwrap_or(0..0); + let buffer_byte_range = buffer_byte_range.unwrap_or_else(|| { + //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk + let row = first_deletion_buffer_row.unwrap(); + let anchor = buffer.anchor_before(Point::new(row, 0)); + let offset = anchor.to_offset(buffer); + offset..offset + }); + //unwrap_or addition without deletion let head_byte_range = head_byte_range.unwrap_or(0..0); From e72e132ce29588f7cfa712c013a209fa5e19af75 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 14 Sep 2022 18:44:00 -0400 Subject: [PATCH 16/73] Clear out commented code & once again perform full file diff on update --- crates/language/src/git.rs | 144 ++++--------------------------------- 1 file changed, 12 insertions(+), 132 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index f2adee42fab42f98ae1374e6033bc9fe3e075fd7..02cf3ca1415b0eb4550ee224c6c9e5fdd7a73b3d 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -202,96 +202,20 @@ impl BufferDiff { } pub fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { - // let buffer_string = buffer.as_rope().to_string(); - // let buffer_bytes = buffer_string.as_bytes(); - - // let mut options = GitOptions::default(); - // options.context_lines(0); - // let patch = match GitPatch::from_buffers( - // head_text.as_bytes(), - // None, - // buffer_bytes, - // None, - // Some(&mut options), - // ) { - // Ok(patch) => patch, - // Err(_) => todo!("This needs to be handled"), - // }; - - // let mut hunks = SumTree::>::new(); - // let mut delta = 0i64; - // for hunk_index in 0..patch.num_hunks() { - // for line_index in 0..patch.num_lines_in_hunk(hunk_index).unwrap() { - // let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); - - // let hunk = match line.origin_value() { - // GitDiffLineType::Addition => { - // let buffer_start = line.content_offset(); - // let buffer_end = buffer_start as usize + line.content().len(); - // let head_offset = (buffer_start - delta) as usize; - // delta += line.content().len() as i64; - // DiffHunk { - // buffer_range: buffer.anchor_before(buffer_start as usize) - // ..buffer.anchor_after(buffer_end), - // head_byte_range: head_offset..head_offset, - // } - // } - - // GitDiffLineType::Deletion => { - // let head_start = line.content_offset(); - // let head_end = head_start as usize + line.content().len(); - // let buffer_offset = (head_start + delta) as usize; - // delta -= line.content().len() as i64; - // DiffHunk { - // buffer_range: buffer.anchor_before(buffer_offset) - // ..buffer.anchor_after(buffer_offset), - // head_byte_range: (head_start as usize)..head_end, - // } - // } - - // _ => continue, - // }; - - // let mut combined = false; - // hunks.update_last( - // |last_hunk| { - // if last_hunk.head_byte_range.end == hunk.head_byte_range.start { - // last_hunk.head_byte_range.end = hunk.head_byte_range.end; - // last_hunk.buffer_range.end = hunk.buffer_range.end; - // combined = true; - // } - // }, - // buffer, - // ); - // if !combined { - // hunks.push(hunk, buffer); - // } - // } - // } - - // println!("====="); - // for hunk in hunks.iter() { - // let buffer_range = hunk.buffer_range.to_point(&buffer); - // println!( - // "hunk in buffer range {buffer_range:?}, head slice {:?}", - // &head_text[hunk.head_byte_range.clone()] - // ); - // } - // println!("====="); - - // self.snapshot.tree = hunks; - } + let mut tree = SumTree::new(); - pub fn actual_update( - &mut self, - head_text: &str, - buffer: &BufferSnapshot, - ) -> Option> { - for edit_range in self.group_edit_ranges(buffer) { - // let patch = self.diff(head, current)?; + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(&head_text, &buffer_text); + + if let Some(patch) = patch { + for hunk_index in 0..patch.num_hunks() { + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer); + tree.push(hunk, buffer); + } } - None + self.last_update_version = buffer.version().clone(); + self.snapshot.tree = tree; } fn diff<'a>(head: &'a str, current: &'a str) -> Option> { @@ -316,7 +240,7 @@ impl BufferDiff { } } - fn group_edit_ranges(&mut self, buffer: &text::BufferSnapshot) -> Vec> { + fn group_edit_ranges(&self, buffer: &text::BufferSnapshot) -> Vec> { const EXPAND_BY: u32 = 20; const COMBINE_DISTANCE: u32 = 5; @@ -338,7 +262,6 @@ impl BufferDiff { } } - self.last_update_version = buffer.version().clone(); ranges } @@ -410,49 +333,6 @@ impl BufferDiff { head_byte_range, } } - - fn name() { - // if self.hunk_index >= self.patch.num_hunks() { - // return None; - // } - - // let mut line_iter = HunkLineIter::new(&self.patch, self.hunk_index); - // let line = line_iter.find(|line| { - // matches!( - // line.origin_value(), - // GitDiffLineType::Addition | GitDiffLineType::Deletion - // ) - // })?; - - // //For the first line of a hunk the content offset is equally valid for an addition or deletion - // let content_offset = line.content_offset() as usize; - - // let mut buffer_range = content_offset..content_offset; - // let mut head_byte_range = match line.origin_value() { - // GitDiffLineType::Addition => content_offset..content_offset, - // GitDiffLineType::Deletion => content_offset..content_offset + line.content().len(), - // _ => unreachable!(), - // }; - - // for line in line_iter { - // match line.origin_value() { - // GitDiffLineType::Addition => { - // // buffer_range.end = - // } - - // GitDiffLineType::Deletion => {} - - // _ => continue, - // } - // } - - // self.hunk_index += 1; - // Some(DiffHunk { - // buffer_range: buffer.anchor_before(buffer_range.start) - // ..buffer.anchor_before(buffer_range.end), - // head_byte_range, - // }) - } } #[cfg(test)] From 03b6f3e0bf2628b238a8e34f40242ddc387a62ca Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 15 Sep 2022 13:57:57 -0400 Subject: [PATCH 17/73] Reorganize for for purely file level invalidation --- crates/language/src/git.rs | 91 +++++--------------------------------- 1 file changed, 10 insertions(+), 81 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 02cf3ca1415b0eb4550ee224c6c9e5fdd7a73b3d..c3f43e54e119e3d7c31274d3f903b6ca17d327d1 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,13 +1,10 @@ use std::ops::Range; -use sum_tree::{Bias, SumTree}; -use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; +use sum_tree::SumTree; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToOffset, ToPoint}; pub use git2 as libgit; -use libgit::{ - DiffLine as GitDiffLine, DiffLineType as GitDiffLineType, DiffOptions as GitOptions, - Patch as GitPatch, -}; +use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; #[derive(Debug, Clone, Copy)] pub enum DiffHunkStatus { @@ -99,40 +96,6 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { } } -struct HunkLineIter<'a, 'b> { - patch: &'a GitPatch<'b>, - hunk_index: usize, - line_index: usize, -} - -impl<'a, 'b> HunkLineIter<'a, 'b> { - fn new(patch: &'a GitPatch<'b>, hunk_index: usize) -> Self { - HunkLineIter { - patch, - hunk_index, - line_index: 0, - } - } -} - -impl<'a, 'b> std::iter::Iterator for HunkLineIter<'a, 'b> { - type Item = GitDiffLine<'b>; - - fn next(&mut self) -> Option { - if self.line_index >= self.patch.num_lines_in_hunk(self.hunk_index).unwrap() { - return None; - } - - let line_index = self.line_index; - self.line_index += 1; - Some( - self.patch - .line_in_hunk(self.hunk_index, line_index) - .unwrap(), - ) - } -} - #[derive(Clone)] pub struct BufferDiffSnapshot { tree: SumTree>, @@ -171,30 +134,22 @@ impl BufferDiffSnapshot { } pub struct BufferDiff { - last_update_version: clock::Global, snapshot: BufferDiffSnapshot, } impl BufferDiff { pub fn new(head_text: &Option, buffer: &text::BufferSnapshot) -> BufferDiff { - let mut tree = SumTree::new(); + let mut instance = BufferDiff { + snapshot: BufferDiffSnapshot { + tree: SumTree::new(), + }, + }; if let Some(head_text) = head_text { - let buffer_text = buffer.as_rope().to_string(); - let patch = Self::diff(&head_text, &buffer_text); - - if let Some(patch) = patch { - for hunk_index in 0..patch.num_hunks() { - let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer); - tree.push(hunk, buffer); - } - } + instance.update(head_text, buffer); } - BufferDiff { - last_update_version: buffer.version().clone(), - snapshot: BufferDiffSnapshot { tree }, - } + instance } pub fn snapshot(&self) -> BufferDiffSnapshot { @@ -214,7 +169,6 @@ impl BufferDiff { } } - self.last_update_version = buffer.version().clone(); self.snapshot.tree = tree; } @@ -240,31 +194,6 @@ impl BufferDiff { } } - fn group_edit_ranges(&self, buffer: &text::BufferSnapshot) -> Vec> { - const EXPAND_BY: u32 = 20; - const COMBINE_DISTANCE: u32 = 5; - - // let mut cursor = self.snapshot.tree.cursor::(); - - let mut ranges = Vec::>::new(); - - for edit in buffer.edits_since::(&self.last_update_version) { - let buffer_start = edit.new.start.row.saturating_sub(EXPAND_BY); - let buffer_end = (edit.new.end.row + EXPAND_BY).min(buffer.row_count()); - - match ranges.last_mut() { - Some(last_range) if last_range.end.abs_diff(buffer_end) <= COMBINE_DISTANCE => { - last_range.start = last_range.start.min(buffer_start); - last_range.end = last_range.end.max(buffer_end); - } - - _ => ranges.push(buffer_start..buffer_end), - } - } - - ranges - } - fn process_patch_hunk<'a>( patch: &GitPatch<'a>, hunk_index: usize, From 446bf886555c52d9871f7214b35cd5fc1455e6ac Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 15 Sep 2022 16:17:17 -0400 Subject: [PATCH 18/73] Use row range while building buffer range during diff line iteration --- crates/language/src/git.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index c3f43e54e119e3d7c31274d3f903b6ca17d327d1..040121fcf257f04d49530313b78554ef249141f5 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,7 +1,7 @@ use std::ops::Range; use sum_tree::SumTree; -use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToOffset, ToPoint}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToPoint}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; @@ -203,7 +203,7 @@ impl BufferDiff { assert!(line_item_count > 0); let mut first_deletion_buffer_row: Option = None; - let mut buffer_byte_range: Option> = None; + let mut buffer_row_range: Option> = None; let mut head_byte_range: Option> = None; for line_index in 0..line_item_count { @@ -212,15 +212,16 @@ impl BufferDiff { let content_offset = line.content_offset() as isize; let content_len = line.content().len() as isize; - match (kind, &mut buffer_byte_range, &mut head_byte_range) { + match (kind, &mut buffer_row_range, &mut head_byte_range) { (GitDiffLineType::Addition, None, _) => { - let end = content_offset + content_len; - buffer_byte_range = Some(content_offset as usize..end as usize); + //guarenteed to be present for additions + let row = line.new_lineno().unwrap().saturating_sub(1); + buffer_row_range = Some(row..row + 1); } (GitDiffLineType::Addition, Some(buffer_byte_range), _) => { - let end = content_offset + content_len; - buffer_byte_range.end = end as usize; + let row = line.new_lineno().unwrap().saturating_sub(1); + buffer_byte_range.end = row + 1; } (GitDiffLineType::Deletion, _, None) => { @@ -245,20 +246,20 @@ impl BufferDiff { } //unwrap_or deletion without addition - let buffer_byte_range = buffer_byte_range.unwrap_or_else(|| { + let buffer_byte_range = buffer_row_range.unwrap_or_else(|| { //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk let row = first_deletion_buffer_row.unwrap(); - let anchor = buffer.anchor_before(Point::new(row, 0)); - let offset = anchor.to_offset(buffer); - offset..offset + row..row }); //unwrap_or addition without deletion let head_byte_range = head_byte_range.unwrap_or(0..0); + let start = Point::new(buffer_byte_range.start, 0); + let end = Point::new(buffer_byte_range.end, 0); + let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); DiffHunk { - buffer_range: buffer.anchor_before(buffer_byte_range.start) - ..buffer.anchor_before(buffer_byte_range.end), + buffer_range, head_byte_range, } } From b9d84df1274a6a75f17715d6b5c32870e6edb2d6 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 15 Sep 2022 17:44:01 -0400 Subject: [PATCH 19/73] Track buffer row divergence while iterating through diff lines This allows for offsetting head row index of deleted lines to normalize into buffer row space --- crates/editor/src/element.rs | 2 +- crates/language/src/git.rs | 52 +++++++++++++++++------------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3a5166e17e9be115174ec371be6b6baab3925c9d..40b1e62adfd9aed287c462433c39686f25eadb70 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -552,7 +552,7 @@ impl EditorElement { //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { - let row = hunk.buffer_range.start as i64 - 1; + let row = hunk.buffer_range.start; let offset = line_height / 2.; let start_y = row as f32 * line_height + offset - scroll_top; diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 040121fcf257f04d49530313b78554ef249141f5..9065ef560620cfb1c84cf5dd91ddede0de36a18f 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -163,8 +163,9 @@ impl BufferDiff { let patch = Self::diff(&head_text, &buffer_text); if let Some(patch) = patch { + let mut divergence = 0; for hunk_index in 0..patch.num_hunks() { - let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer); + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); tree.push(hunk, buffer); } } @@ -198,6 +199,7 @@ impl BufferDiff { patch: &GitPatch<'a>, hunk_index: usize, buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, ) -> DiffHunk { let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); assert!(line_item_count > 0); @@ -212,41 +214,35 @@ impl BufferDiff { let content_offset = line.content_offset() as isize; let content_len = line.content().len() as isize; - match (kind, &mut buffer_row_range, &mut head_byte_range) { - (GitDiffLineType::Addition, None, _) => { - //guarenteed to be present for additions - let row = line.new_lineno().unwrap().saturating_sub(1); - buffer_row_range = Some(row..row + 1); - } + if kind == GitDiffLineType::Addition { + *buffer_row_divergence += 1; + let row = line.new_lineno().unwrap().saturating_sub(1); - (GitDiffLineType::Addition, Some(buffer_byte_range), _) => { - let row = line.new_lineno().unwrap().saturating_sub(1); - buffer_byte_range.end = row + 1; + match &mut buffer_row_range { + Some(buffer_row_range) => buffer_row_range.end = row + 1, + None => buffer_row_range = Some(row..row + 1), } + } - (GitDiffLineType::Deletion, _, None) => { - let end = content_offset + content_len; - head_byte_range = Some(content_offset as usize..end as usize); - } + if kind == GitDiffLineType::Deletion { + *buffer_row_divergence -= 1; + let end = content_offset + content_len; - (GitDiffLineType::Deletion, _, Some(head_byte_range)) => { - let end = content_offset + content_len; - head_byte_range.end = end as usize; + match &mut head_byte_range { + Some(head_byte_range) => head_byte_range.end = end as usize, + None => head_byte_range = Some(content_offset as usize..end as usize), } - _ => {} - } - - if kind == GitDiffLineType::Deletion && first_deletion_buffer_row.is_none() { - //old_lineno is guarenteed to be Some for deletions - //libgit gives us line numbers that are 1-indexed but also returns a 0 for some states - let row = line.old_lineno().unwrap().saturating_sub(1); - first_deletion_buffer_row = Some(row); + if first_deletion_buffer_row.is_none() { + let old_row = line.old_lineno().unwrap().saturating_sub(1); + let row = old_row as i64 + *buffer_row_divergence; + first_deletion_buffer_row = Some(row as u32); + } } } //unwrap_or deletion without addition - let buffer_byte_range = buffer_row_range.unwrap_or_else(|| { + let buffer_row_range = buffer_row_range.unwrap_or_else(|| { //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk let row = first_deletion_buffer_row.unwrap(); row..row @@ -255,8 +251,8 @@ impl BufferDiff { //unwrap_or addition without deletion let head_byte_range = head_byte_range.unwrap_or(0..0); - let start = Point::new(buffer_byte_range.start, 0); - let end = Point::new(buffer_byte_range.end, 0); + let start = Point::new(buffer_row_range.start, 0); + let end = Point::new(buffer_row_range.end, 0); let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); DiffHunk { buffer_range, From c4da8c46f70ba8e3c2e475547e3c97bba785dec4 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 15 Sep 2022 18:50:31 -0400 Subject: [PATCH 20/73] Disable unnecessary libgit2 cargo features Co-Authored-By: Mikayla Maki --- Cargo.lock | 18 ------------------ crates/language/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2872d83a943df07d9bdeff6f8d1f6d336d9eba15..1f60f1d36ca158f8c45faabcc19d57c549233aca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2234,8 +2234,6 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", - "openssl-sys", "url", ] @@ -2918,9 +2916,7 @@ checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" dependencies = [ "cc", "libc", - "libssh2-sys", "libz-sys", - "openssl-sys", "pkg-config", ] @@ -2964,20 +2960,6 @@ 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" diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6d347f3595312ab8415ee123b56823df3fadee6a..034b10e89c64d201dbff0ab3409900ba9558c2a8 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -51,7 +51,7 @@ smol = "1.2" tree-sitter = "0.20" tree-sitter-rust = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true } -git2 = "0.15" +git2 = { version = "0.15", default-features = false } [dev-dependencies] client = { path = "../client", features = ["test-support"] } From 9c8295487752b7982a0277a2b949abd98e81e004 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 15 Sep 2022 19:06:45 -0400 Subject: [PATCH 21/73] Changed diffs to be async and dropped git delay --- crates/editor/src/multi_buffer.rs | 12 +++--- crates/language/src/buffer.rs | 44 +++++++++++++++------ crates/language/src/git.rs | 63 ++++++++++++++----------------- crates/workspace/src/workspace.rs | 2 +- 4 files changed, 70 insertions(+), 51 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 1d09b7008fbd20fd59fb29b80741cada2306216d..72b88f837d2768b565026a5e58001035b598431a 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -313,11 +313,13 @@ impl MultiBuffer { } pub fn update_git(&mut self, cx: &mut ModelContext) { - let mut buffers = self.buffers.borrow_mut(); - for buffer in buffers.values_mut() { - buffer.buffer.update(cx, |buffer, _| { - buffer.update_git(); - }) + let buffers = self.buffers.borrow(); + for buffer_state in buffers.values() { + if buffer_state.buffer.read(cx).needs_git_update() { + buffer_state + .buffer + .update(cx, |buffer, cx| buffer.update_git(cx)) + } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e75e17e54134cc92ffa120c3d5a2305c3cf617b3..2cff3796bc1927d736b69227a2b323bd51dd0219 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,4 +1,4 @@ -use crate::git::{BufferDiff, BufferDiffSnapshot, DiffHunk}; +use crate::git::{BufferDiff, DiffHunk}; pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, @@ -48,7 +48,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, - head_text: Option, + head_text: Option>, git_diff: BufferDiff, file: Option>, saved_version: clock::Global, @@ -77,7 +77,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub diff_snapshot: BufferDiffSnapshot, + pub diff_snapshot: BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -347,7 +347,7 @@ impl Buffer { ) -> Self { Self::build( TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), - head_text.map(|h| h.into()), + head_text.map(|h| Arc::new(h.into())), Some(file), ) } @@ -358,7 +358,7 @@ impl Buffer { file: Option>, ) -> Result { let buffer = TextBuffer::new(replica_id, message.id, message.base_text); - let mut this = Self::build(buffer, message.head_text, file); + let mut this = Self::build(buffer, message.head_text.map(|text| Arc::new(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"))?, @@ -414,14 +414,18 @@ impl Buffer { self } - fn build(buffer: TextBuffer, head_text: Option, 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 { UNIX_EPOCH }; - let git_diff = BufferDiff::new(&head_text, &buffer); + let git_diff = smol::block_on(BufferDiff::new(head_text.clone(), &buffer)); Self { saved_mtime, @@ -462,7 +466,7 @@ impl Buffer { BufferSnapshot { text, syntax, - diff_snapshot: self.git_diff.snapshot(), + diff_snapshot: self.git_diff.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -650,11 +654,29 @@ impl Buffer { task } - pub fn update_git(&mut self) { - if let Some(head_text) = &self.head_text { + pub fn needs_git_update(&self) -> bool { + self.git_diff.needs_update(self) + } + + pub fn update_git(&mut self, cx: &mut ModelContext) { + if self.head_text.is_some() { let snapshot = self.snapshot(); - self.git_diff.update(head_text, &snapshot); + let head_text = self.head_text.clone(); self.diff_update_count += 1; + + let buffer_diff = cx + .background() + .spawn(async move { BufferDiff::new(head_text, &snapshot).await }); + + cx.spawn_weak(|this, mut cx| async move { + let buffer_diff = buffer_diff.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + this.git_diff = buffer_diff; + }) + } + }) + .detach() } } diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 9065ef560620cfb1c84cf5dd91ddede0de36a18f..65ac373f7aa7aa8966400a0c5430b9307ca77f7c 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use sum_tree::SumTree; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToPoint}; @@ -97,11 +97,25 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { } #[derive(Clone)] -pub struct BufferDiffSnapshot { +pub struct BufferDiff { + last_buffer_version: clock::Global, tree: SumTree>, } -impl BufferDiffSnapshot { +impl BufferDiff { + pub async fn new(head_text: Option>, buffer: &text::BufferSnapshot) -> BufferDiff { + let mut instance = BufferDiff { + last_buffer_version: buffer.version().clone(), + tree: SumTree::new(), + }; + + if let Some(head_text) = head_text { + instance.update(&*head_text, buffer); + } + + instance + } + pub fn hunks_in_range<'a>( &'a self, query_row_range: Range, @@ -127,36 +141,11 @@ impl BufferDiffSnapshot { }) } - #[cfg(test)] - fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { - self.hunks_in_range(0..u32::MAX, text) + pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool { + buffer.version().changed_since(&self.last_buffer_version) } -} - -pub struct BufferDiff { - snapshot: BufferDiffSnapshot, -} -impl BufferDiff { - pub fn new(head_text: &Option, buffer: &text::BufferSnapshot) -> BufferDiff { - let mut instance = BufferDiff { - snapshot: BufferDiffSnapshot { - tree: SumTree::new(), - }, - }; - - if let Some(head_text) = head_text { - instance.update(head_text, buffer); - } - - instance - } - - pub fn snapshot(&self) -> BufferDiffSnapshot { - self.snapshot.clone() - } - - pub fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { + fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { let mut tree = SumTree::new(); let buffer_text = buffer.as_rope().to_string(); @@ -170,7 +159,13 @@ impl BufferDiff { } } - self.snapshot.tree = tree; + self.tree = tree; + self.last_buffer_version = buffer.version().clone(); + } + + #[cfg(test)] + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + self.hunks_in_range(0..u32::MAX, text) } fn diff<'a>(head: &'a str, current: &'a str) -> Option> { @@ -284,7 +279,7 @@ mod tests { .unindent(); let mut buffer = Buffer::new(0, 0, buffer_text); - let diff = BufferDiff::new(&Some(head_text.clone()), &buffer); + let diff = smol::block_on(BufferDiff::new(Some(Arc::new(head_text.clone())), &buffer)); assert_hunks(&diff, &buffer, &head_text, &[(1..2, "two\n")]); buffer.edit([(0..0, "point five\n")]); @@ -298,7 +293,7 @@ mod tests { head_text: &str, expected_hunks: &[(Range, &str)], ) { - let hunks = diff.snapshot.hunks(buffer).collect::>(); + let hunks = diff.hunks(buffer).collect::>(); assert_eq!( hunks.len(), expected_hunks.len(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ad3862c56ff2a60e7715f9858780b37c5e344fef..e28e4d66d1acba19bdd1714de679201c4a160452 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -734,7 +734,7 @@ impl ItemHandle for ViewHandle { ); } - const GIT_DELAY: Duration = Duration::from_millis(600); + const GIT_DELAY: Duration = Duration::from_millis(10); let item = item.clone(); pending_git_update.fire_new( GIT_DELAY, From 6825b6077aa8120f4b5978c64d68fde122b39419 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 16 Sep 2022 12:20:31 -0400 Subject: [PATCH 22/73] Properly invalidate when async git diff completes --- crates/editor/src/items.rs | 1 - crates/language/src/buffer.rs | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d208fc9c15819a1ffee304f585bd5dc857b839bb..76e148018089913944fe0ca5379ce76f23658d28 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -486,7 +486,6 @@ impl Item for Editor { self.buffer().update(cx, |multibuffer, cx| { multibuffer.update_git(cx); }); - cx.notify(); Task::ready(Ok(())) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2cff3796bc1927d736b69227a2b323bd51dd0219..5ddebcaff610e836ddd1b1a1a9849ee88eab5628 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -662,7 +662,6 @@ impl Buffer { if self.head_text.is_some() { let snapshot = self.snapshot(); let head_text = self.head_text.clone(); - self.diff_update_count += 1; let buffer_diff = cx .background() @@ -671,8 +670,10 @@ impl Buffer { cx.spawn_weak(|this, mut cx| async move { let buffer_diff = buffer_diff.await; if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { this.git_diff = buffer_diff; + this.diff_update_count += 1; + cx.notify(); }) } }) From 6633c0b3287484bd7b051f2de5bb49fba8fc6379 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 16 Sep 2022 14:49:24 -0400 Subject: [PATCH 23/73] Perform initial file load git diff async --- crates/language/src/buffer.rs | 18 +++++++++--------- crates/language/src/git.rs | 28 +++++++++++++--------------- crates/project/src/worktree.rs | 6 +++++- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5ddebcaff610e836ddd1b1a1a9849ee88eab5628..90e86a20c472dc50e71545cf4251a778c92ce450 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -425,8 +425,6 @@ impl Buffer { UNIX_EPOCH }; - let git_diff = smol::block_on(BufferDiff::new(head_text.clone(), &buffer)); - Self { saved_mtime, saved_version: buffer.version(), @@ -435,7 +433,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, head_text, - git_diff, + git_diff: BufferDiff::new(), file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, @@ -659,16 +657,18 @@ impl Buffer { } pub fn update_git(&mut self, cx: &mut ModelContext) { - if self.head_text.is_some() { + if let Some(head_text) = &self.head_text { let snapshot = self.snapshot(); - let head_text = self.head_text.clone(); + let head_text = head_text.clone(); - let buffer_diff = cx - .background() - .spawn(async move { BufferDiff::new(head_text, &snapshot).await }); + let mut diff = self.git_diff.clone(); + let diff = cx.background().spawn(async move { + diff.update(&head_text, &snapshot).await; + diff + }); cx.spawn_weak(|this, mut cx| async move { - let buffer_diff = buffer_diff.await; + let buffer_diff = diff.await; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { this.git_diff = buffer_diff; diff --git a/crates/language/src/git.rs b/crates/language/src/git.rs index 65ac373f7aa7aa8966400a0c5430b9307ca77f7c..d713dcbc144ed8560e38091f8084d3654cac9f4f 100644 --- a/crates/language/src/git.rs +++ b/crates/language/src/git.rs @@ -1,4 +1,4 @@ -use std::{ops::Range, sync::Arc}; +use std::ops::Range; use sum_tree::SumTree; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToPoint}; @@ -98,22 +98,16 @@ impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { #[derive(Clone)] pub struct BufferDiff { - last_buffer_version: clock::Global, + last_buffer_version: Option, tree: SumTree>, } impl BufferDiff { - pub async fn new(head_text: Option>, buffer: &text::BufferSnapshot) -> BufferDiff { - let mut instance = BufferDiff { - last_buffer_version: buffer.version().clone(), + pub fn new() -> BufferDiff { + BufferDiff { + last_buffer_version: None, tree: SumTree::new(), - }; - - if let Some(head_text) = head_text { - instance.update(&*head_text, buffer); } - - instance } pub fn hunks_in_range<'a>( @@ -142,10 +136,13 @@ impl BufferDiff { } pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool { - buffer.version().changed_since(&self.last_buffer_version) + match &self.last_buffer_version { + Some(last) => buffer.version().changed_since(last), + None => true, + } } - fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { + pub async fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { let mut tree = SumTree::new(); let buffer_text = buffer.as_rope().to_string(); @@ -160,7 +157,7 @@ impl BufferDiff { } self.tree = tree; - self.last_buffer_version = buffer.version().clone(); + self.last_buffer_version = Some(buffer.version().clone()); } #[cfg(test)] @@ -279,7 +276,8 @@ mod tests { .unindent(); let mut buffer = Buffer::new(0, 0, buffer_text); - let diff = smol::block_on(BufferDiff::new(Some(Arc::new(head_text.clone())), &buffer)); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&head_text, &buffer)); assert_hunks(&diff, &buffer, &head_text, &[(1..2, "two\n")]); buffer.edit([(0..0, "point five\n")]); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 42d18eb3bbdba06ff80b35a51bf2ca9aa4632031..2ff3e6fe0406f5d84558dc2841f3398fc800c659 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -449,7 +449,11 @@ impl LocalWorktree { 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, head_text, Arc::new(file), cx))) + Ok(cx.add_model(|cx| { + let mut buffer = Buffer::from_file(0, contents, head_text, Arc::new(file), cx); + buffer.update_git(cx); + buffer + })) }) } From 8edee9b2a8680bc5db074e76eec7a400ca92caf1 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 16 Sep 2022 16:48:43 -0400 Subject: [PATCH 24/73] Async-ify head text loading --- crates/project/src/worktree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2ff3e6fe0406f5d84558dc2841f3398fc800c659..79e2ed9da94c46dc01ae297d5f025c1a037cdfaf 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -573,7 +573,13 @@ impl LocalWorktree { 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; + + let head_text = { + let fs = fs.clone(); + let abs_path = abs_path.clone(); + let task = async move { fs.load_head_text(&abs_path).await }; + cx.background().spawn(task).await + }; // Eagerly populate the snapshot with an updated entry for the loaded file let entry = this From b18dd8fcff7a87f46fbc98b247b9e2d3198abc4f Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Sep 2022 15:16:04 -0400 Subject: [PATCH 25/73] Fully qualify outside git-related code when a diff is a git diff --- crates/editor/src/display_map/fold_map.rs | 2 +- crates/editor/src/element.rs | 2 +- crates/editor/src/multi_buffer.rs | 33 ++++++++++---------- crates/language/src/buffer.rs | 38 +++++++++++------------ 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 6ab5c6202ea3637db18ddf9fff31780983634ce8..c17cfa39f2a7292198a1c953339e315824fe73b8 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -274,7 +274,7 @@ impl FoldMap { if buffer.edit_count() != new_buffer.edit_count() || buffer.parse_count() != new_buffer.parse_count() || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count() - || buffer.diff_update_count() != new_buffer.diff_update_count() + || buffer.git_diff_update_count() != new_buffer.git_diff_update_count() || buffer.trailing_excerpt_update_count() != new_buffer.trailing_excerpt_update_count() { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 40b1e62adfd9aed287c462433c39686f25eadb70..a2935145597ff9726e50860bc2e6ef4661d57c5c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1477,7 +1477,7 @@ impl Element for EditorElement { let diff_hunks = snapshot .buffer_snapshot - .diff_hunks_in_range(start_row..end_row) + .git_diff_hunks_in_range(start_row..end_row) .collect(); let mut max_visible_line_width = 0.0; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 72b88f837d2768b565026a5e58001035b598431a..2f93bc5b0930f9cd118dbe90a06f9c62b8668ba3 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -91,7 +91,7 @@ struct BufferState { last_selections_update_count: usize, last_diagnostics_update_count: usize, last_file_update_count: usize, - last_diff_update_count: usize, + last_git_diff_update_count: usize, excerpts: Vec, _subscriptions: [gpui::Subscription; 2], } @@ -103,7 +103,7 @@ pub struct MultiBufferSnapshot { parse_count: usize, diagnostics_update_count: usize, trailing_excerpt_update_count: usize, - diff_update_count: usize, + git_diff_update_count: usize, edit_count: usize, is_dirty: bool, has_conflict: bool, @@ -205,7 +205,7 @@ impl MultiBuffer { last_selections_update_count: buffer_state.last_selections_update_count, last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, last_file_update_count: buffer_state.last_file_update_count, - last_diff_update_count: buffer_state.last_diff_update_count, + last_git_diff_update_count: buffer_state.last_git_diff_update_count, excerpts: buffer_state.excerpts.clone(), _subscriptions: [ new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), @@ -842,7 +842,7 @@ impl MultiBuffer { last_selections_update_count: buffer_snapshot.selections_update_count(), last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(), last_file_update_count: buffer_snapshot.file_update_count(), - last_diff_update_count: buffer_snapshot.diff_update_count(), + last_git_diff_update_count: buffer_snapshot.git_diff_update_count(), excerpts: Default::default(), _subscriptions: [ cx.observe(&buffer, |_, _, cx| cx.notify()), @@ -1265,7 +1265,7 @@ impl MultiBuffer { let mut excerpts_to_edit = Vec::new(); let mut reparsed = false; let mut diagnostics_updated = false; - let mut diff_updated = false; + let mut git_diff_updated = false; let mut is_dirty = false; let mut has_conflict = false; let mut edited = false; @@ -1277,7 +1277,7 @@ impl MultiBuffer { let selections_update_count = buffer.selections_update_count(); let diagnostics_update_count = buffer.diagnostics_update_count(); let file_update_count = buffer.file_update_count(); - let diff_update_count = buffer.diff_update_count(); + let git_diff_update_count = buffer.git_diff_update_count(); let buffer_edited = version.changed_since(&buffer_state.last_version); let buffer_reparsed = parse_count > buffer_state.last_parse_count; @@ -1286,20 +1286,21 @@ impl MultiBuffer { let buffer_diagnostics_updated = diagnostics_update_count > buffer_state.last_diagnostics_update_count; let buffer_file_updated = file_update_count > buffer_state.last_file_update_count; - let buffer_diff_updated = diff_update_count > buffer_state.last_diff_update_count; + let buffer_git_diff_updated = + git_diff_update_count > buffer_state.last_git_diff_update_count; if buffer_edited || buffer_reparsed || buffer_selections_updated || buffer_diagnostics_updated || buffer_file_updated - || buffer_diff_updated + || buffer_git_diff_updated { buffer_state.last_version = version; buffer_state.last_parse_count = parse_count; buffer_state.last_selections_update_count = selections_update_count; buffer_state.last_diagnostics_update_count = diagnostics_update_count; buffer_state.last_file_update_count = file_update_count; - buffer_state.last_diff_update_count = diff_update_count; + buffer_state.last_git_diff_update_count = git_diff_update_count; excerpts_to_edit.extend( buffer_state .excerpts @@ -1311,7 +1312,7 @@ impl MultiBuffer { edited |= buffer_edited; reparsed |= buffer_reparsed; diagnostics_updated |= buffer_diagnostics_updated; - diff_updated |= buffer_diff_updated; + git_diff_updated |= buffer_git_diff_updated; is_dirty |= buffer.is_dirty(); has_conflict |= buffer.has_conflict(); } @@ -1324,8 +1325,8 @@ impl MultiBuffer { if diagnostics_updated { snapshot.diagnostics_update_count += 1; } - if diff_updated { - snapshot.diff_update_count += 1; + if git_diff_updated { + snapshot.git_diff_update_count += 1; } snapshot.is_dirty = is_dirty; snapshot.has_conflict = has_conflict; @@ -2504,8 +2505,8 @@ impl MultiBufferSnapshot { self.diagnostics_update_count } - pub fn diff_update_count(&self) -> usize { - self.diff_update_count + pub fn git_diff_update_count(&self) -> usize { + self.git_diff_update_count } pub fn trailing_excerpt_update_count(&self) -> usize { @@ -2558,13 +2559,13 @@ impl MultiBufferSnapshot { }) } - pub fn diff_hunks_in_range<'a>( + pub fn git_diff_hunks_in_range<'a>( &'a self, row_range: Range, ) -> impl 'a + Iterator> { self.as_singleton() .into_iter() - .flat_map(move |(_, _, buffer)| buffer.diff_hunks_in_range(row_range.clone())) + .flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone())) } pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 90e86a20c472dc50e71545cf4251a778c92ce450..53bfe4a10ce21b10336c75ca3d7fde4d2e6fdd99 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,4 +1,4 @@ -use crate::git::{BufferDiff, DiffHunk}; +use crate::git; pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, @@ -49,7 +49,7 @@ pub use lsp::DiagnosticSeverity; pub struct Buffer { text: TextBuffer, head_text: Option>, - git_diff: BufferDiff, + git_diff: git::BufferDiff, file: Option>, saved_version: clock::Global, saved_version_fingerprint: String, @@ -69,7 +69,7 @@ pub struct Buffer { diagnostics_update_count: usize, diagnostics_timestamp: clock::Lamport, file_update_count: usize, - diff_update_count: usize, + git_diff_update_count: usize, completion_triggers: Vec, completion_triggers_timestamp: clock::Lamport, deferred_ops: OperationQueue, @@ -77,13 +77,13 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub diff_snapshot: BufferDiff, + pub git_diff_snapshot: git::BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, diagnostics_update_count: usize, file_update_count: usize, - diff_update_count: usize, + git_diff_update_count: usize, remote_selections: TreeMap, selections_update_count: usize, language: Option>, @@ -433,7 +433,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, head_text, - git_diff: BufferDiff::new(), + git_diff: git::BufferDiff::new(), file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, @@ -448,7 +448,7 @@ impl Buffer { diagnostics_update_count: 0, diagnostics_timestamp: Default::default(), file_update_count: 0, - diff_update_count: 0, + git_diff_update_count: 0, completion_triggers: Default::default(), completion_triggers_timestamp: Default::default(), deferred_ops: OperationQueue::new(), @@ -464,13 +464,13 @@ impl Buffer { BufferSnapshot { text, syntax, - diff_snapshot: self.git_diff.clone(), + git_diff_snapshot: self.git_diff.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - diff_update_count: self.diff_update_count, + git_diff_update_count: self.git_diff_update_count, language: self.language.clone(), parse_count: self.parse_count, selections_update_count: self.selections_update_count, @@ -672,7 +672,7 @@ impl Buffer { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { this.git_diff = buffer_diff; - this.diff_update_count += 1; + this.git_diff_update_count += 1; cx.notify(); }) } @@ -705,8 +705,8 @@ impl Buffer { self.file_update_count } - pub fn diff_update_count(&self) -> usize { - self.diff_update_count + pub fn git_diff_update_count(&self) -> usize { + self.git_diff_update_count } #[cfg(any(test, feature = "test-support"))] @@ -2191,11 +2191,11 @@ impl BufferSnapshot { }) } - pub fn diff_hunks_in_range<'a>( + pub fn git_diff_hunks_in_range<'a>( &'a self, query_row_range: Range, - ) -> impl 'a + Iterator> { - self.diff_snapshot.hunks_in_range(query_row_range, self) + ) -> impl 'a + Iterator> { + self.git_diff_snapshot.hunks_in_range(query_row_range, self) } pub fn diagnostics_in_range<'a, T, O>( @@ -2246,8 +2246,8 @@ impl BufferSnapshot { self.file_update_count } - pub fn diff_update_count(&self) -> usize { - self.diff_update_count + pub fn git_diff_update_count(&self) -> usize { + self.git_diff_update_count } } @@ -2275,7 +2275,7 @@ impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { text: self.text.clone(), - diff_snapshot: self.diff_snapshot.clone(), + git_diff_snapshot: self.git_diff_snapshot.clone(), syntax: self.syntax.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), @@ -2283,7 +2283,7 @@ impl Clone for BufferSnapshot { selections_update_count: self.selections_update_count, diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - diff_update_count: self.diff_update_count, + git_diff_update_count: self.git_diff_update_count, language: self.language.clone(), parse_count: self.parse_count, } From a679557e40f7eafd9aa615d2741a266e9c5987b9 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Sep 2022 18:22:39 -0400 Subject: [PATCH 26/73] Avoid racing git diffs & allow for "as fast as possible" diff updating Co-Authored-By: Mikayla Maki --- assets/settings/default.json | 4 +++ crates/language/src/buffer.rs | 38 +++++++++++++++++++------- crates/settings/src/settings.rs | 18 +++++++++++++ crates/workspace/src/workspace.rs | 45 +++++++++++++++++++++++-------- 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index a12cf44d94ae29c45851d3b39a3c4caa32008f96..4ebc1e702fda87db0128d6c969dd94f364a07ea7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -74,6 +74,10 @@ "hard_tabs": false, // How many columns a tab should occupy. "tab_size": 4, + // Git gutter behavior configuration. Remove this item to disable git gutters entirely. + "git_gutter": { + "files_included": "all" + }, // Settings specific to the terminal "terminal": { // What shell to use when opening a terminal. May take 3 values: diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 53bfe4a10ce21b10336c75ca3d7fde4d2e6fdd99..6ecfbc7e6293f75bad776817dbf5fc1c25f92ba8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -46,10 +46,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript}; pub use lsp::DiagnosticSeverity; +struct GitDiffStatus { + diff: git::BufferDiff, + update_in_progress: bool, + update_requested: bool, +} + pub struct Buffer { text: TextBuffer, head_text: Option>, - git_diff: git::BufferDiff, + git_diff_status: GitDiffStatus, file: Option>, saved_version: clock::Global, saved_version_fingerprint: String, @@ -77,7 +83,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub git_diff_snapshot: git::BufferDiff, + pub git_diff: git::BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -433,7 +439,11 @@ impl Buffer { was_dirty_before_starting_transaction: None, text: buffer, head_text, - git_diff: git::BufferDiff::new(), + git_diff_status: GitDiffStatus { + diff: git::BufferDiff::new(), + update_in_progress: false, + update_requested: false, + }, file, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, @@ -464,7 +474,7 @@ impl Buffer { BufferSnapshot { text, syntax, - git_diff_snapshot: self.git_diff.clone(), + git_diff: self.git_diff_status.diff.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -653,15 +663,20 @@ impl Buffer { } pub fn needs_git_update(&self) -> bool { - self.git_diff.needs_update(self) + self.git_diff_status.diff.needs_update(self) } pub fn update_git(&mut self, cx: &mut ModelContext) { + if self.git_diff_status.update_in_progress { + self.git_diff_status.update_requested = true; + return; + } + if let Some(head_text) = &self.head_text { let snapshot = self.snapshot(); let head_text = head_text.clone(); - let mut diff = self.git_diff.clone(); + let mut diff = self.git_diff_status.diff.clone(); let diff = cx.background().spawn(async move { diff.update(&head_text, &snapshot).await; diff @@ -671,9 +686,14 @@ impl Buffer { let buffer_diff = diff.await; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.git_diff = buffer_diff; + this.git_diff_status.diff = buffer_diff; this.git_diff_update_count += 1; cx.notify(); + + this.git_diff_status.update_in_progress = false; + if this.git_diff_status.update_requested { + this.update_git(cx); + } }) } }) @@ -2195,7 +2215,7 @@ impl BufferSnapshot { &'a self, query_row_range: Range, ) -> impl 'a + Iterator> { - self.git_diff_snapshot.hunks_in_range(query_row_range, self) + self.git_diff.hunks_in_range(query_row_range, self) } pub fn diagnostics_in_range<'a, T, O>( @@ -2275,7 +2295,7 @@ impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { text: self.text.clone(), - git_diff_snapshot: self.git_diff_snapshot.clone(), + git_diff: self.git_diff.clone(), syntax: self.syntax.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index e346ff60e6ba89a304e43b7e8696c90d09ac88cb..adb2892b3682af0a8170c634be54879e91369a43 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -61,6 +61,22 @@ pub struct EditorSettings { pub format_on_save: Option, pub formatter: Option, pub enable_language_server: Option, + pub git_gutter: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] +pub struct GitGutterConfig { + pub files_included: GitGutterLevel, + pub debounce_delay_millis: Option, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterLevel { + #[default] + All, + OnlyTracked, + None, } #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] @@ -250,6 +266,7 @@ impl Settings { format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), + git_gutter: defaults.editor.git_gutter, }, editor_overrides: Default::default(), terminal_defaults: Default::default(), @@ -378,6 +395,7 @@ impl Settings { format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), + git_gutter: Default::default(), }, editor_overrides: Default::default(), terminal_defaults: Default::default(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e28e4d66d1acba19bdd1714de679201c4a160452..9e8338d2896294237642af9c8facf564c9af8d54 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -734,18 +734,41 @@ impl ItemHandle for ViewHandle { ); } - const GIT_DELAY: Duration = Duration::from_millis(10); + let debounce_delay = cx + .global::() + .editor_overrides + .git_gutter + .unwrap_or_default() + .debounce_delay_millis; let item = item.clone(); - pending_git_update.fire_new( - GIT_DELAY, - workspace, - cx, - |project, mut cx| async move { - cx.update(|cx| item.update_git(project, cx)) - .await - .log_err(); - }, - ); + + if let Some(delay) = debounce_delay { + const MIN_GIT_DELAY: u64 = 50; + + let delay = delay.max(MIN_GIT_DELAY); + let duration = Duration::from_millis(delay); + + pending_git_update.fire_new( + duration, + workspace, + cx, + |project, mut cx| async move { + cx.update(|cx| item.update_git(project, cx)) + .await + .log_err(); + }, + ); + } else { + let project = workspace.project().downgrade(); + cx.spawn_weak(|_, mut cx| async move { + if let Some(project) = project.upgrade(&cx) { + cx.update(|cx| item.update_git(project, cx)) + .await + .log_err(); + } + }) + .detach(); + } } _ => {} From 632f47930f30e175c81e109c448431b156906600 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Sep 2022 18:44:47 -0400 Subject: [PATCH 27/73] Utilize initial file contents as head text by default Co-Authored-By: Mikayla Maki --- crates/project/src/worktree.rs | 24 ++++++++++++++++++++++-- crates/settings/src/settings.rs | 6 +++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 79e2ed9da94c46dc01ae297d5f025c1a037cdfaf..4d2b5097381f580bb6e27ce242501f4c1a5fd023 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -32,6 +32,7 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; +use settings::Settings; use smol::channel::{self, Sender}; use std::{ any::Any, @@ -571,14 +572,33 @@ impl LocalWorktree { let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); + + let files_included = cx + .global::() + .editor_overrides + .git_gutter + .unwrap_or_default() + .files_included; + cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let head_text = { + let head_text = if matches!( + files_included, + settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked + ) { let fs = fs.clone(); let abs_path = abs_path.clone(); let task = async move { fs.load_head_text(&abs_path).await }; - cx.background().spawn(task).await + let results = cx.background().spawn(task).await; + + if files_included == settings::GitFilesIncluded::All { + results.or_else(|| Some(text.clone())) + } else { + results + } + } else { + None }; // Eagerly populate the snapshot with an updated entry for the loaded file diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index adb2892b3682af0a8170c634be54879e91369a43..3f4a764c79641e268f06d6ab78939577910856bc 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -66,13 +66,13 @@ pub struct EditorSettings { #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct GitGutterConfig { - pub files_included: GitGutterLevel, + pub files_included: GitFilesIncluded, pub debounce_delay_millis: Option, } -#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] -pub enum GitGutterLevel { +pub enum GitFilesIncluded { #[default] All, OnlyTracked, From 8d2de1074b1b7583c5ed10cf401ec3a92de291bb Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Sep 2022 19:25:59 -0400 Subject: [PATCH 28/73] Pull git indicator colors out of theme Co-Authored-By: Kay Simmons Co-Authored-By: Mikayla Maki --- crates/editor/src/element.rs | 15 ++++++++++++--- crates/theme/src/theme.rs | 1 + styles/src/styleTree/editor.ts | 6 ++++-- styles/src/themes/common/base16.ts | 5 +++++ styles/src/themes/common/theme.ts | 1 + 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a2935145597ff9726e50860bc2e6ef4661d57c5c..2e767d72e67cc9880227b8d0c29c59f1d58ee432 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -545,10 +545,19 @@ impl EditorElement { } } + let (inserted_color, modified_color, deleted_color) = { + let editor = &cx.global::().theme.editor; + ( + editor.diff_background_inserted, + editor.diff_background_modified, + editor.diff_background_deleted, + ) + }; + for hunk in &layout.diff_hunks { let color = match hunk.status() { - DiffHunkStatus::Added => Color::green(), - DiffHunkStatus::Modified => Color::blue(), + DiffHunkStatus::Added => inserted_color, + DiffHunkStatus::Modified => modified_color, //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { @@ -565,7 +574,7 @@ impl EditorElement { cx.scene.push_quad(Quad { bounds: highlight_bounds, - background: Some(Color::red()), + background: Some(deleted_color), border: Border::new(0., Color::transparent_black()), corner_radius: 1. * line_height, }); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 739a4c76869b82b9ab066bb32eff8bb58a0cd253..1fd586efeeb0a6b948c04022139400e886c202ab 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -490,6 +490,7 @@ pub struct Editor { pub document_highlight_write_background: Color, pub diff_background_deleted: Color, pub diff_background_inserted: Color, + pub diff_background_modified: Color, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 62f7a0efdfcbb68b1f0503edd3ea326f7eafa97d..29d6857964999bfdff0cdbf7f6be78c58c7f5b6b 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -7,6 +7,7 @@ import { player, popoverShadow, text, + textColor, TextColor, } from "./components"; import hoverPopover from "./hoverPopover"; @@ -59,8 +60,9 @@ export default function editor(theme: Theme) { indicator: iconColor(theme, "secondary"), verticalScale: 0.618 }, - diffBackgroundDeleted: backgroundColor(theme, "error"), - diffBackgroundInserted: backgroundColor(theme, "ok"), + diffBackgroundDeleted: theme.ramps.red(0.3).hex(), + diffBackgroundInserted: theme.ramps.green(0.3).hex(), + diffBackgroundModified: theme.ramps.blue(0.3).hex(), documentHighlightReadBackground: theme.editor.highlight.occurrence, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence, errorColor: theme.textColor.error, diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 7aa72ef1377ea40656a45e50bc4155f28ac7f8a5..326928252e837c7784cc4db2217a00f2905f0fae 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -113,6 +113,11 @@ export function createTheme( hovered: sample(ramps.blue, 0.1), active: sample(ramps.blue, 0.15), }, + on500Ok: { + base: sample(ramps.green, 0.05), + hovered: sample(ramps.green, 0.1), + active: sample(ramps.green, 0.15) + } }; const borderColor = { diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index e01435b846c4d4a5d1fdbe8366166c38de361f07..b93148ae2cff53809a7a4fe390c532e8ef726fa7 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -78,6 +78,7 @@ export default interface Theme { // Hacks for elements on top of the editor on500: BackgroundColorSet; ok: BackgroundColorSet; + on500Ok: BackgroundColorSet; error: BackgroundColorSet; on500Error: BackgroundColorSet; warning: BackgroundColorSet; From bb8798a8444eb96af94236ba41ea0652b91baf59 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 20 Sep 2022 17:50:29 -0400 Subject: [PATCH 29/73] WIP pls amend me Co-Authored-By: Max Brunsfeld Co-Authored-By: Mikayla Maki --- Cargo.lock | 1 + crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 2 +- crates/project/src/worktree.rs | 76 +++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f60f1d36ca158f8c45faabcc19d57c549233aca..040db0fd41089d344d5d0c2cff5ebee79da3cd66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3996,6 +3996,7 @@ dependencies = [ "fsevent", "futures", "fuzzy", + "git2", "gpui", "ignore", "language", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index a4ea6f22864df3a51a99d59994fe0755bbaf576a..76eef0efa71f834b7952c962f19aebafaf9b4199 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 = { version = "0.15", default-features = false } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8fa1fe962289935321cba03015339c987277dd47..4d16c6ad1f5dde53ef1b7a5121e2f32f0dd94d22 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4538,7 +4538,7 @@ impl Project { }) .detach(); } - + let push_strong_handle = { let worktree = worktree.read(cx); self.is_shared() || worktree.is_visible() || worktree.is_remote() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 4d2b5097381f580bb6e27ce242501f4c1a5fd023..b054f9332884e48774611f4652684f02df5b40fe 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -27,7 +27,7 @@ use language::{ Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope, }; use lazy_static::lazy_static; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use postage::{ prelude::{Sink as _, Stream as _}, watch, @@ -105,12 +105,20 @@ pub struct Snapshot { pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, + git_repositories: Vec, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, extension_counts: HashMap, } +#[derive(Clone)] +pub(crate) struct GitRepositoryState { + content_path: Arc, + git_dir_path: Arc, + repository: Arc>, +} + impl Deref for LocalSnapshot { type Target = Snapshot; @@ -143,6 +151,7 @@ struct ShareState { pub enum Event { UpdatedEntries, + UpdatedGitRepositories(Vec), } impl Entity for Worktree { @@ -373,6 +382,7 @@ impl LocalWorktree { let mut snapshot = LocalSnapshot { abs_path, ignores_by_parent_abs_path: Default::default(), + git_repositories: Default::default(), removed_entry_ids: Default::default(), next_entry_id, snapshot: Snapshot { @@ -504,6 +514,7 @@ impl LocalWorktree { fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext) { self.poll_task.take(); + match self.scan_state() { ScanState::Idle => { self.snapshot = self.background_snapshot.lock().clone(); @@ -512,6 +523,7 @@ impl LocalWorktree { } cx.emit(Event::UpdatedEntries); } + ScanState::Initializing => { let is_fake_fs = self.fs.is_fake(); self.snapshot = self.background_snapshot.lock().clone(); @@ -528,12 +540,14 @@ impl LocalWorktree { })); cx.emit(Event::UpdatedEntries); } + _ => { if force { self.snapshot = self.background_snapshot.lock().clone(); } } } + cx.notify(); } @@ -1285,6 +1299,10 @@ impl LocalSnapshot { pub fn extension_counts(&self) -> &HashMap { &self.extension_counts } + + pub(crate) fn git_repository_for_file_path(&self, path: &Path) -> Option { + None + } #[cfg(test)] pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { @@ -3042,6 +3060,61 @@ mod tests { assert!(tree.entry_for_path(".git").unwrap().is_ignored); }); } + + #[gpui::test] + async fn test_git_repository_for_path(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + ".git": {}, + "deps": { + "dep1": { + ".git": {}, + "src": { + "a.txt": "" + } + } + }, + "src": { + "b.txt": "" + } + }, + "c.txt": "" + }), + ) + .await; + + let http_client = FakeHttpClient::with_404_response(); + let client = Client::new(http_client); + let tree = Worktree::local( + client, + Arc::from(Path::new("/root")), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + tree.read_with(cx, |tree, cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree.git_repository_for_file_path("c.txt".as_ref()).is_none()); + + let repo1 = tree.git_repository_for_file_path("dir1/src/b.txt".as_ref()).unwrap().lock(); + assert_eq!(repo1.content_path.as_ref(), Path::new("dir1")); + assert_eq!(repo1.git_dir_path.as_ref(), Path::new("dir1/.git")); + + let repo2 = tree.git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()).unwrap().lock(); + assert_eq!(repo2.content_path.as_ref(), Path::new("dir1/deps/dep1")); + assert_eq!(repo2.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git")); + }); + } #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { @@ -3161,6 +3234,7 @@ mod tests { abs_path: root_dir.path().into(), removed_entry_ids: Default::default(), ignores_by_parent_abs_path: Default::default(), + git_repositories: Default::default(), next_entry_id: next_entry_id.clone(), snapshot: Snapshot { id: WorktreeId::from_usize(0), From 0d1b2a7e4693f38464bbdfe41fb9b10a03d501e8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 12:50:35 -0700 Subject: [PATCH 30/73] WIP - max & mikayla working on tests --- crates/project/src/project.rs | 3 +- crates/project/src/worktree.rs | 134 +++++++++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 17 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d16c6ad1f5dde53ef1b7a5121e2f32f0dd94d22..36c7c6cf8130ab6ec5a30aeb265171147719b0bc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4535,10 +4535,11 @@ impl Project { if worktree.read(cx).is_local() { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), + worktree::Event::UpdatedGitRepositories(_) => todo!(), }) .detach(); } - + let push_strong_handle = { let worktree = worktree.read(cx); self.is_shared() || worktree.is_visible() || worktree.is_remote() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b054f9332884e48774611f4652684f02df5b40fe..49dbe061176ff7171ed3df872a312572e6e0812f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -18,6 +18,7 @@ use futures::{ Stream, StreamExt, }; use fuzzy::CharBag; +use git2::Repository; use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, @@ -27,7 +28,7 @@ use language::{ Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope, }; use lazy_static::lazy_static; -use parking_lot::{Mutex, RwLock}; +use parking_lot::Mutex; use postage::{ prelude::{Sink as _, Stream as _}, watch, @@ -41,6 +42,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, + mem, ops::{Deref, DerefMut}, os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, @@ -52,6 +54,7 @@ use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use util::{ResultExt, TryFutureExt}; lazy_static! { + static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); } @@ -101,6 +104,24 @@ pub struct Snapshot { is_complete: bool, } +// + +// 'GitResolver' +// File paths <-> Repository Paths -> git_repository_path() -> First .git in an ancestor in a path +// Repository Paths <-> Repository Pointers -> git_repository_open() +// fs.watch() ^ +// +// Folder: where all the git magic happens +// .git IT +// OR it can be a file that points somewhere else + +// 1. Walk through the file tree, looking for .git files or folders +// 2. When we discover them, open and save a libgit2 pointer to the repository +// 2a. Use git_repository_path() to start a watch on the repository (if not already watched) +// +// File paths -> Git repository == Ancestor check (is there a .git in an ancestor folder) +// Git repository -> Files == Descendent check (subtracting out any intersecting .git folders) + #[derive(Clone)] pub struct LocalSnapshot { abs_path: Arc, @@ -113,9 +134,10 @@ pub struct LocalSnapshot { } #[derive(Clone)] -pub(crate) struct GitRepositoryState { +pub struct GitRepositoryState { content_path: Arc, git_dir_path: Arc, + scan_id: usize, repository: Arc>, } @@ -1299,11 +1321,34 @@ impl LocalSnapshot { pub fn extension_counts(&self) -> &HashMap { &self.extension_counts } - + pub(crate) fn git_repository_for_file_path(&self, path: &Path) -> Option { + for repository in self.git_repositories.iter().rev() { + if path.starts_with(&repository.content_path) { + return Some(repository.clone()); + } + } + None + } + + pub(crate) fn git_repository_for_git_data(&self, path: &Path) -> Option { + for repository in self.git_repositories.iter() { + if path.starts_with(&repository.git_dir_path) { + return Some(repository.clone()); + } + } None } + pub(crate) fn does_git_repository_track_file_path( + &self, + repo: &GitRepositoryState, + file_path: &Path, + ) -> bool { + self.git_repository_for_file_path(file_path) + .map_or(false, |r| r.content_path == repo.content_path) + } + #[cfg(test)] pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { let root_name = self.root_name.clone(); @@ -1403,6 +1448,25 @@ impl LocalSnapshot { ); } } + } else if entry.path.file_name() == Some(&DOT_GIT) { + let abs_path = self.abs_path.join(&entry.path); + let content_path: Arc = entry.path.parent().unwrap().into(); + if let Err(ix) = self + .git_repositories + .binary_search_by_key(&&content_path, |repo| &repo.content_path) + { + if let Some(repository) = Repository::open(&abs_path).log_err() { + self.git_repositories.insert( + ix, + GitRepositoryState { + content_path, + git_dir_path: repository.path().into(), + scan_id: self.scan_id, + repository: Arc::new(Mutex::new(repository)), + }, + ); + } + } } self.reuse_entry_id(&mut entry); @@ -1549,6 +1613,14 @@ impl LocalSnapshot { { *scan_id = self.snapshot.scan_id; } + } else if path.file_name() == Some(&DOT_GIT) { + let parent_path = path.parent().unwrap(); + if let Ok(ix) = self + .git_repositories + .binary_search_by_key(&parent_path, |repo| repo.content_path.as_ref()) + { + self.git_repositories[ix].scan_id = self.snapshot.scan_id; + } } } @@ -2423,6 +2495,7 @@ impl BackgroundScanner { self.snapshot.lock().removed_entry_ids.clear(); self.update_ignore_statuses().await; + self.update_git_repositories().await; true } @@ -2488,6 +2561,16 @@ impl BackgroundScanner { .await; } + async fn update_git_repositories(&self) { + let mut snapshot = self.snapshot(); + let mut git_repositories = mem::take(&mut snapshot.git_repositories); + git_repositories.retain(|git_repository| { + let dot_git_path = git_repository.content_path.join(&*DOT_GIT); + snapshot.entry_for_path(dot_git_path).is_some() + }); + snapshot.git_repositories = git_repositories; + } + async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { let mut ignore_stack = job.ignore_stack; if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { @@ -3060,7 +3143,7 @@ mod tests { assert!(tree.entry_for_path(".git").unwrap().is_ignored); }); } - + #[gpui::test] async fn test_git_repository_for_path(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background()); @@ -3068,7 +3151,9 @@ mod tests { "/root", json!({ "dir1": { - ".git": {}, + ".git": { + "HEAD": "abc" + }, "deps": { "dep1": { ".git": {}, @@ -3097,22 +3182,39 @@ mod tests { &mut cx.to_async(), ) .await - .unwrap(); - + .unwrap(); + cx.foreground().run_until_parked(); - + tree.read_with(cx, |tree, cx| { let tree = tree.as_local().unwrap(); - - assert!(tree.git_repository_for_file_path("c.txt".as_ref()).is_none()); - let repo1 = tree.git_repository_for_file_path("dir1/src/b.txt".as_ref()).unwrap().lock(); - assert_eq!(repo1.content_path.as_ref(), Path::new("dir1")); - assert_eq!(repo1.git_dir_path.as_ref(), Path::new("dir1/.git")); + assert!(tree + .git_repository_for_file_path("c.txt".as_ref()) + .is_none()); + + let repo = tree + .git_repository_for_file_path("dir1/src/b.txt".as_ref()) + .unwrap(); + + // Need to update the file system for anything involving git + // Goal: Make this test pass + // Up Next: Invalidating git repos! + assert_eq!(repo.content_path.as_ref(), Path::new("dir1")); + assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git")); + + let repo = tree + .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()) + .unwrap(); + + assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); + assert_eq!( repo = tree .git_repository_for_git_data("dir/.git/HEAD".as_ref()) + .unwrap(); + assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); - let repo2 = tree.git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()).unwrap().lock(); - assert_eq!(repo2.content_path.as_ref(), Path::new("dir1/deps/dep1")); - assert_eq!(repo2.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git")); + assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref())); + assert!(!tree + .does_git_repository_track_file_path(&repo, "dir1/deps/dep1/src/a.txt".as_ref())); }); } From 6ac9308a034cd480357355c2566c44464aaf9058 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 22 Sep 2022 16:55:24 -0700 Subject: [PATCH 31/73] Added git repository type infrastructure and moved git file system stuff into fs abstraction so we can test without touching the file system. Co-Authored-By: kay@zed.dev --- crates/project/src/fs.rs | 27 ++++++ crates/project/src/git_repository.rs | 132 +++++++++++++++++++++++++ crates/project/src/project.rs | 1 + crates/project/src/worktree.rs | 139 ++++++++++++++------------- 4 files changed, 232 insertions(+), 67 deletions(-) create mode 100644 crates/project/src/git_repository.rs diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index a983df0f4b4eb20de30c945235b506883344bc37..70d18798861cb4dee5e5524710948f697b1f58f0 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -12,6 +12,7 @@ use std::{ pin::Pin, time::{Duration, SystemTime}, }; + use text::Rope; #[cfg(any(test, feature = "test-support"))] @@ -21,6 +22,8 @@ use futures::lock::Mutex; #[cfg(any(test, feature = "test-support"))] use std::sync::{Arc, Weak}; +use crate::git_repository::{FakeGitRepository, GitRepository, RealGitRepository}; + #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; @@ -45,6 +48,11 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; + fn open_git_repository( + &self, + abs_dotgit_path: &Path, + content_path: &Arc, + ) -> Option>; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; @@ -270,6 +278,14 @@ impl Fs for RealFs { }))) } + fn open_git_repository( + &self, + abs_dotgit_path: &Path, + content_path: &Arc, + ) -> Option> { + RealGitRepository::open(abs_dotgit_path, content_path) + } + fn is_fake(&self) -> bool { false } @@ -885,6 +901,17 @@ impl Fs for FakeFs { })) } + fn open_git_repository( + &self, + abs_dotgit_path: &Path, + content_path: &Arc, + ) -> Option> { + Some(Box::new(FakeGitRepository::new( + abs_dotgit_path, + content_path, + ))) + } + fn is_fake(&self) -> bool { true } diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe7747be9be42ddcac318d5ade6b8eb5abf47427 --- /dev/null +++ b/crates/project/src/git_repository.rs @@ -0,0 +1,132 @@ +use git2::Repository; +use parking_lot::Mutex; +use std::{path::Path, sync::Arc}; +use util::ResultExt; + +pub trait GitRepository: Send + Sync { + fn boxed_clone(&self) -> Box; + fn is_path_managed_by(&self, path: &Path) -> bool; + fn is_path_in_git_folder(&self, path: &Path) -> bool; + fn content_path(&self) -> &Path; + fn git_dir_path(&self) -> &Path; + fn last_scan_id(&self) -> usize; + fn set_scan_id(&mut self, scan_id: usize); +} + +#[derive(Clone)] +pub struct RealGitRepository { + // Path to folder containing the .git file or directory + content_path: Arc, + // Path to the actual .git folder. + // Note: if .git is a file, this points to the folder indicated by the .git file + git_dir_path: Arc, + last_scan_id: usize, + libgit_repository: Arc>, +} + +impl RealGitRepository { + pub fn open( + abs_dotgit_path: &Path, + content_path: &Arc, + ) -> Option> { + Repository::open(&abs_dotgit_path) + .log_err() + .map::, _>(|libgit_repository| { + Box::new(Self { + content_path: content_path.clone(), + git_dir_path: libgit_repository.path().into(), + last_scan_id: 0, + libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)), + }) + }) + } +} + +impl GitRepository for RealGitRepository { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn is_path_managed_by(&self, path: &Path) -> bool { + path.starts_with(&self.content_path) + } + + fn is_path_in_git_folder(&self, path: &Path) -> bool { + path.starts_with(&self.git_dir_path) + } + + fn content_path(&self) -> &Path { + self.content_path.as_ref() + } + + fn git_dir_path(&self) -> &Path { + self.git_dir_path.as_ref() + } + + fn last_scan_id(&self) -> usize { + self.last_scan_id + } + + fn set_scan_id(&mut self, scan_id: usize) { + self.last_scan_id = scan_id; + } +} + +impl PartialEq for &Box { + fn eq(&self, other: &Self) -> bool { + self.content_path() == other.content_path() + } +} +impl Eq for &Box {} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] +pub struct FakeGitRepository { + // Path to folder containing the .git file or directory + content_path: Arc, + // Path to the actual .git folder. + // Note: if .git is a file, this points to the folder indicated by the .git file + git_dir_path: Arc, + last_scan_id: usize, +} + +impl FakeGitRepository { + pub fn new(abs_dotgit_path: &Path, content_path: &Arc) -> FakeGitRepository { + Self { + content_path: content_path.clone(), + git_dir_path: abs_dotgit_path.into(), + last_scan_id: 0, + } + } +} + +#[cfg(any(test, feature = "test-support"))] +impl GitRepository for FakeGitRepository { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn is_path_managed_by(&self, path: &Path) -> bool { + path.starts_with(&self.content_path) + } + + fn is_path_in_git_folder(&self, path: &Path) -> bool { + path.starts_with(&self.git_dir_path) + } + + fn content_path(&self) -> &Path { + self.content_path.as_ref() + } + + fn git_dir_path(&self) -> &Path { + self.git_dir_path.as_ref() + } + + fn last_scan_id(&self) -> usize { + self.last_scan_id + } + + fn set_scan_id(&mut self, scan_id: usize) { + self.last_scan_id = scan_id; + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 36c7c6cf8130ab6ec5a30aeb265171147719b0bc..78a500585a3f2bbf997a5c4bdd98c05c5cc04446 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,4 +1,5 @@ pub mod fs; +mod git_repository; mod ignore; mod lsp_command; pub mod search; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 49dbe061176ff7171ed3df872a312572e6e0812f..5ae8bf542cd89e7ee6be96f798de53089f85ad4f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,4 +1,4 @@ -use crate::{copy_recursive, ProjectEntryId, RemoveOptions}; +use crate::{copy_recursive, git_repository::GitRepository, ProjectEntryId, RemoveOptions}; use super::{ fs::{self, Fs}, @@ -18,7 +18,6 @@ use futures::{ Stream, StreamExt, }; use fuzzy::CharBag; -use git2::Repository; use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, @@ -104,41 +103,32 @@ pub struct Snapshot { is_complete: bool, } -// - -// 'GitResolver' -// File paths <-> Repository Paths -> git_repository_path() -> First .git in an ancestor in a path -// Repository Paths <-> Repository Pointers -> git_repository_open() -// fs.watch() ^ -// -// Folder: where all the git magic happens -// .git IT -// OR it can be a file that points somewhere else - -// 1. Walk through the file tree, looking for .git files or folders -// 2. When we discover them, open and save a libgit2 pointer to the repository -// 2a. Use git_repository_path() to start a watch on the repository (if not already watched) -// -// File paths -> Git repository == Ancestor check (is there a .git in an ancestor folder) -// Git repository -> Files == Descendent check (subtracting out any intersecting .git folders) - -#[derive(Clone)] pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories: Vec, + git_repositories: Vec>, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, extension_counts: HashMap, } -#[derive(Clone)] -pub struct GitRepositoryState { - content_path: Arc, - git_dir_path: Arc, - scan_id: usize, - repository: Arc>, +impl Clone for LocalSnapshot { + fn clone(&self) -> Self { + Self { + abs_path: self.abs_path.clone(), + ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(), + git_repositories: self + .git_repositories + .iter() + .map(|repo| repo.boxed_clone()) + .collect(), + removed_entry_ids: self.removed_entry_ids.clone(), + next_entry_id: self.next_entry_id.clone(), + snapshot: self.snapshot.clone(), + extension_counts: self.extension_counts.clone(), + } + } } impl Deref for LocalSnapshot { @@ -173,7 +163,7 @@ struct ShareState { pub enum Event { UpdatedEntries, - UpdatedGitRepositories(Vec), + UpdatedGitRepositories(Vec>), } impl Entity for Worktree { @@ -1322,31 +1312,47 @@ impl LocalSnapshot { &self.extension_counts } - pub(crate) fn git_repository_for_file_path(&self, path: &Path) -> Option { - for repository in self.git_repositories.iter().rev() { - if path.starts_with(&repository.content_path) { - return Some(repository.clone()); - } - } - None - } - - pub(crate) fn git_repository_for_git_data(&self, path: &Path) -> Option { - for repository in self.git_repositories.iter() { - if path.starts_with(&repository.git_dir_path) { - return Some(repository.clone()); - } - } - None + // Gives the most specific git repository for a given path + pub(crate) fn git_repository_for_file_path( + &self, + path: &Path, + ) -> Option> { + self.git_repositories + .iter() + .rev() //git_repository is ordered lexicographically + .find(|repo| repo.is_path_managed_by(path)) + .map(|repo| repo.boxed_clone()) + } + + // ~/zed: + // - src + // - crates + // - .git -> /usr/.git + pub(crate) fn git_repository_for_git_data( + &self, + path: &Path, + ) -> Option> { + self.git_repositories + .iter() + .find(|repo| repo.is_path_in_git_folder(path)) + .map(|repo| repo.boxed_clone()) } pub(crate) fn does_git_repository_track_file_path( &self, - repo: &GitRepositoryState, + repo: &Box, file_path: &Path, ) -> bool { + // /zed + // - .git + // - a.txt + // - /dep + // - b.txt + // - .git + + // Depends on git_repository_for_file_path returning the most specific git repository for a given path self.git_repository_for_file_path(file_path) - .map_or(false, |r| r.content_path == repo.content_path) + .map_or(false, |r| &r == repo) } #[cfg(test)] @@ -1431,7 +1437,7 @@ impl LocalSnapshot { } fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { - if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { + if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { @@ -1453,18 +1459,10 @@ impl LocalSnapshot { let content_path: Arc = entry.path.parent().unwrap().into(); if let Err(ix) = self .git_repositories - .binary_search_by_key(&&content_path, |repo| &repo.content_path) + .binary_search_by_key(&content_path.as_ref(), |repo| repo.content_path()) { - if let Some(repository) = Repository::open(&abs_path).log_err() { - self.git_repositories.insert( - ix, - GitRepositoryState { - content_path, - git_dir_path: repository.path().into(), - scan_id: self.scan_id, - repository: Arc::new(Mutex::new(repository)), - }, - ); + if let Some(repository) = fs.open_git_repository(&abs_path, &content_path) { + self.git_repositories.insert(ix, repository); } } } @@ -1617,9 +1615,9 @@ impl LocalSnapshot { let parent_path = path.parent().unwrap(); if let Ok(ix) = self .git_repositories - .binary_search_by_key(&parent_path, |repo| repo.content_path.as_ref()) + .binary_search_by_key(&parent_path, |repo| repo.content_path().as_ref()) { - self.git_repositories[ix].scan_id = self.snapshot.scan_id; + self.git_repositories[ix].set_scan_id(self.snapshot.scan_id); } } } @@ -2565,7 +2563,7 @@ impl BackgroundScanner { let mut snapshot = self.snapshot(); let mut git_repositories = mem::take(&mut snapshot.git_repositories); git_repositories.retain(|git_repository| { - let dot_git_path = git_repository.content_path.join(&*DOT_GIT); + let dot_git_path = git_repository.content_path().join(&*DOT_GIT); snapshot.entry_for_path(dot_git_path).is_some() }); snapshot.git_repositories = git_repositories; @@ -2925,6 +2923,7 @@ mod tests { fmt::Write, time::{SystemTime, UNIX_EPOCH}, }; + use util::test::temp_tree; #[gpui::test] @@ -3147,6 +3146,7 @@ mod tests { #[gpui::test] async fn test_git_repository_for_path(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background()); + fs.insert_tree( "/root", json!({ @@ -3200,17 +3200,22 @@ mod tests { // Need to update the file system for anything involving git // Goal: Make this test pass // Up Next: Invalidating git repos! - assert_eq!(repo.content_path.as_ref(), Path::new("dir1")); - assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git")); + assert_eq!(repo.content_path(), Path::new("dir1")); + assert_eq!(repo.git_dir_path(), Path::new("dir1/.git")); let repo = tree .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); - assert_eq!( repo = tree .git_repository_for_git_data("dir/.git/HEAD".as_ref()) + assert_eq!(repo.content_path(), Path::new("dir1/deps/dep1")); + assert_eq!(repo.git_dir_path(), Path::new("dir1/deps/dep1")); + + let repo = tree + .git_repository_for_git_data("dir1/.git/HEAD".as_ref()) .unwrap(); - assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); + + assert_eq!(repo.content_path(), Path::new("dir1")); + assert_eq!(repo.git_dir_path(), Path::new("dir1/.git")); assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref())); assert!(!tree From c8e63d76a41596a56cd15d0804d0e4cba631b509 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 26 Sep 2022 07:59:51 -0700 Subject: [PATCH 32/73] Get the test to failing,,, correctly --- Cargo.lock | 2 + crates/project/Cargo.toml | 1 + crates/project/src/git_repository.rs | 10 ++++ crates/project/src/worktree.rs | 68 ++++++++++++++-------------- crates/util/Cargo.toml | 6 ++- crates/util/src/lib.rs | 7 +++ crates/util/src/test.rs | 8 ++++ 7 files changed, 68 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 040db0fd41089d344d5d0c2cff5ebee79da3cd66..8157327cf225ce1d4bdf33facc293b18da51bfba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6359,6 +6359,8 @@ version = "0.1.0" dependencies = [ "anyhow", "futures", + "git2", + "lazy_static", "log", "rand 0.8.5", "serde_json", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 76eef0efa71f834b7952c962f19aebafaf9b4199..8ca01eac2c55389bbfa04c57fed0cdfa179c9a8e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -54,6 +54,7 @@ toml = "0.5" rocksdb = "0.18" git2 = { version = "0.15", default-features = false } + [dev-dependencies] client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index fe7747be9be42ddcac318d5ade6b8eb5abf47427..47849bf644cdc91646f0f6ff6c02fbdc20e7d325 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -11,6 +11,7 @@ pub trait GitRepository: Send + Sync { fn git_dir_path(&self) -> &Path; fn last_scan_id(&self) -> usize; fn set_scan_id(&mut self, scan_id: usize); + fn with_repo(&mut self, f: Box); } #[derive(Clone)] @@ -70,6 +71,11 @@ impl GitRepository for RealGitRepository { fn set_scan_id(&mut self, scan_id: usize) { self.last_scan_id = scan_id; } + + fn with_repo(&mut self, f: Box) { + let mut git2 = self.libgit_repository.lock(); + f(&mut git2) + } } impl PartialEq for &Box { @@ -129,4 +135,8 @@ impl GitRepository for FakeGitRepository { fn set_scan_id(&mut self, scan_id: usize) { self.last_scan_id = scan_id; } + + fn with_repo(&mut self, _: Box) { + unimplemented!(); + } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5ae8bf542cd89e7ee6be96f798de53089f85ad4f..6fd00aabbd84c37855e9dae788cf85499b31e8af 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -26,7 +26,6 @@ use language::{ proto::{deserialize_version, serialize_line_ending, serialize_version}, Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope, }; -use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ prelude::{Sink as _, Stream as _}, @@ -50,12 +49,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{ResultExt, TryFutureExt}; - -lazy_static! { - static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); - static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); -} +use util::{ResultExt, TryFutureExt, DOT_GIT, GITIGNORE}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -1317,6 +1311,13 @@ impl LocalSnapshot { &self, path: &Path, ) -> Option> { + let repos = self + .git_repositories + .iter() + .map(|repo| repo.content_path().to_str().unwrap().to_string()) + .collect::>(); + dbg!(repos); + self.git_repositories .iter() .rev() //git_repository is ordered lexicographically @@ -1437,6 +1438,7 @@ impl LocalSnapshot { } fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + dbg!(&entry.path); if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { @@ -1455,6 +1457,8 @@ impl LocalSnapshot { } } } else if entry.path.file_name() == Some(&DOT_GIT) { + dbg!(&entry.path); + let abs_path = self.abs_path.join(&entry.path); let content_path: Arc = entry.path.parent().unwrap().into(); if let Err(ix) = self @@ -2223,6 +2227,7 @@ impl BackgroundScanner { if ignore_stack.is_all() { if let Some(mut root_entry) = snapshot.root_entry().cloned() { root_entry.is_ignored = true; + dbg!("scan dirs entry"); snapshot.insert_entry(root_entry, self.fs.as_ref()); } } @@ -2445,6 +2450,7 @@ impl BackgroundScanner { snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); + dbg!("process_events entry"); snapshot.insert_entry(fs_entry, self.fs.as_ref()); let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -3145,50 +3151,46 @@ mod tests { #[gpui::test] async fn test_git_repository_for_path(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.background()); - - fs.insert_tree( - "/root", - json!({ - "dir1": { - ".git": { - "HEAD": "abc" - }, - "deps": { - "dep1": { - ".git": {}, - "src": { - "a.txt": "" - } + let root = temp_tree(json!({ + "dir1": { + ".git": {}, + "deps": { + "dep1": { + ".git": {}, + "src": { + "a.txt": "" } - }, - "src": { - "b.txt": "" } }, - "c.txt": "" - }), - ) - .await; + "src": { + "b.txt": "" + } + }, + "c.txt": "" + })); let http_client = FakeHttpClient::with_404_response(); let client = Client::new(http_client); let tree = Worktree::local( client, - Arc::from(Path::new("/root")), + root.path(), true, - fs.clone(), + Arc::new(RealFs), Default::default(), &mut cx.to_async(), ) .await .unwrap(); - cx.foreground().run_until_parked(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, cx| { + tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); + dbg!(tree); + assert!(tree .git_repository_for_file_path("c.txt".as_ref()) .is_none()); diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 4ec214fef10d2c4e1a95212fce84b973de0d8336..78416aa5b506556cf76f8d8782489917615a74ba 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -7,17 +7,21 @@ edition = "2021" doctest = false [features] -test-support = ["rand", "serde_json", "tempdir"] +test-support = ["rand", "serde_json", "tempdir", "git2"] [dependencies] anyhow = "1.0.38" futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } +lazy_static = "1.4.0" rand = { version = "0.8", optional = true } tempdir = { version = "0.3.7", optional = true } serde_json = { version = "1.0", features = ["preserve_order"], optional = true } +git2 = { version = "0.15", default-features = false, optional = true } + [dev-dependencies] rand = { version = "0.8" } tempdir = { version = "0.3.7" } serde_json = { version = "1.0", features = ["preserve_order"] } +git2 = { version = "0.15", default-features = false } diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 97f409f410c36c2b2de7fb14e9cc7ef94f4996b1..52bf70e3a7167a926b2aecdda9eb3c6e14f6858f 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -2,13 +2,20 @@ pub mod test; use futures::Future; +use lazy_static::lazy_static; use std::{ cmp::Ordering, + ffi::OsStr, ops::AddAssign, pin::Pin, task::{Context, Poll}, }; +lazy_static! { + pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); + pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} + pub fn truncate(s: &str, max_chars: usize) -> &str { match s.char_indices().nth(max_chars) { None => s, diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 7b2e00d57b00283d0cfb31dfbd4369b041514ff3..4e4716434e28a105f43f8a9248f9a848f52ef74e 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -1,12 +1,15 @@ mod assertions; mod marked_text; +use git2; use std::path::{Path, PathBuf}; use tempdir::TempDir; pub use assertions::*; pub use marked_text::*; +use crate::DOT_GIT; + pub fn temp_tree(tree: serde_json::Value) -> TempDir { let dir = TempDir::new("").unwrap(); write_tree(dir.path(), tree); @@ -24,6 +27,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) { match contents { Value::Object(_) => { fs::create_dir(&path).unwrap(); + + if path.file_name() == Some(&DOT_GIT) { + git2::Repository::init(&path.parent().unwrap()).unwrap(); + } + write_tree(&path, contents); } Value::Null => { From 4251e0f5f13978e6d6a779f30797948e2ac37382 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 26 Sep 2022 16:57:31 -0400 Subject: [PATCH 33/73] Find repos under worktree & return correct results for repo queries Co-Authored-By: Mikayla Maki --- crates/project/src/fs.rs | 27 ++---- crates/project/src/git_repository.rs | 121 +++++---------------------- crates/project/src/worktree.rs | 98 +++++++++------------- 3 files changed, 66 insertions(+), 180 deletions(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 70d18798861cb4dee5e5524710948f697b1f58f0..e675ddf8e58f67218c6526af73db9cd2acd4cd76 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -22,7 +22,7 @@ use futures::lock::Mutex; #[cfg(any(test, feature = "test-support"))] use std::sync::{Arc, Weak}; -use crate::git_repository::{FakeGitRepository, GitRepository, RealGitRepository}; +use crate::git_repository::GitRepository; #[async_trait::async_trait] pub trait Fs: Send + Sync { @@ -48,11 +48,7 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; - fn open_git_repository( - &self, - abs_dotgit_path: &Path, - content_path: &Arc, - ) -> Option>; + fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; @@ -278,12 +274,8 @@ impl Fs for RealFs { }))) } - fn open_git_repository( - &self, - abs_dotgit_path: &Path, - content_path: &Arc, - ) -> Option> { - RealGitRepository::open(abs_dotgit_path, content_path) + fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option { + GitRepository::open(abs_dotgit_path) } fn is_fake(&self) -> bool { @@ -901,15 +893,8 @@ impl Fs for FakeFs { })) } - fn open_git_repository( - &self, - abs_dotgit_path: &Path, - content_path: &Arc, - ) -> Option> { - Some(Box::new(FakeGitRepository::new( - abs_dotgit_path, - content_path, - ))) + fn open_git_repository(&self, _: &Path) -> Option { + None } fn is_fake(&self) -> bool { diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index 47849bf644cdc91646f0f6ff6c02fbdc20e7d325..d1df841fe71d4e55d55d37731b25167a428ce42b 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -3,19 +3,8 @@ use parking_lot::Mutex; use std::{path::Path, sync::Arc}; use util::ResultExt; -pub trait GitRepository: Send + Sync { - fn boxed_clone(&self) -> Box; - fn is_path_managed_by(&self, path: &Path) -> bool; - fn is_path_in_git_folder(&self, path: &Path) -> bool; - fn content_path(&self) -> &Path; - fn git_dir_path(&self) -> &Path; - fn last_scan_id(&self) -> usize; - fn set_scan_id(&mut self, scan_id: usize); - fn with_repo(&mut self, f: Box); -} - #[derive(Clone)] -pub struct RealGitRepository { +pub struct GitRepository { // Path to folder containing the .git file or directory content_path: Arc, // Path to the actual .git folder. @@ -25,118 +14,50 @@ pub struct RealGitRepository { libgit_repository: Arc>, } -impl RealGitRepository { - pub fn open( - abs_dotgit_path: &Path, - content_path: &Arc, - ) -> Option> { - Repository::open(&abs_dotgit_path) +impl GitRepository { + pub fn open(dotgit_path: &Path) -> Option { + Repository::open(&dotgit_path) .log_err() - .map::, _>(|libgit_repository| { - Box::new(Self { - content_path: content_path.clone(), - git_dir_path: libgit_repository.path().into(), + .and_then(|libgit_repository| { + Some(Self { + content_path: libgit_repository.workdir()?.into(), + git_dir_path: dotgit_path.canonicalize().log_err()?.into(), last_scan_id: 0, libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)), }) }) } -} - -impl GitRepository for RealGitRepository { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - fn is_path_managed_by(&self, path: &Path) -> bool { - path.starts_with(&self.content_path) + pub fn is_path_managed_by(&self, path: &Path) -> bool { + path.canonicalize() + .map(|path| path.starts_with(&self.content_path)) + .unwrap_or(false) } - fn is_path_in_git_folder(&self, path: &Path) -> bool { - path.starts_with(&self.git_dir_path) + pub fn is_path_in_git_folder(&self, path: &Path) -> bool { + path.canonicalize() + .map(|path| path.starts_with(&self.git_dir_path)) + .unwrap_or(false) } - fn content_path(&self) -> &Path { + pub fn content_path(&self) -> &Path { self.content_path.as_ref() } - fn git_dir_path(&self) -> &Path { + pub fn git_dir_path(&self) -> &Path { self.git_dir_path.as_ref() } - fn last_scan_id(&self) -> usize { + pub fn last_scan_id(&self) -> usize { self.last_scan_id } - fn set_scan_id(&mut self, scan_id: usize) { + pub fn set_scan_id(&mut self, scan_id: usize) { self.last_scan_id = scan_id; } - fn with_repo(&mut self, f: Box) { + pub fn with_repo(&mut self, f: Box) { let mut git2 = self.libgit_repository.lock(); f(&mut git2) } } - -impl PartialEq for &Box { - fn eq(&self, other: &Self) -> bool { - self.content_path() == other.content_path() - } -} -impl Eq for &Box {} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Clone)] -pub struct FakeGitRepository { - // Path to folder containing the .git file or directory - content_path: Arc, - // Path to the actual .git folder. - // Note: if .git is a file, this points to the folder indicated by the .git file - git_dir_path: Arc, - last_scan_id: usize, -} - -impl FakeGitRepository { - pub fn new(abs_dotgit_path: &Path, content_path: &Arc) -> FakeGitRepository { - Self { - content_path: content_path.clone(), - git_dir_path: abs_dotgit_path.into(), - last_scan_id: 0, - } - } -} - -#[cfg(any(test, feature = "test-support"))] -impl GitRepository for FakeGitRepository { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn is_path_managed_by(&self, path: &Path) -> bool { - path.starts_with(&self.content_path) - } - - fn is_path_in_git_folder(&self, path: &Path) -> bool { - path.starts_with(&self.git_dir_path) - } - - fn content_path(&self) -> &Path { - self.content_path.as_ref() - } - - fn git_dir_path(&self) -> &Path { - self.git_dir_path.as_ref() - } - - fn last_scan_id(&self) -> usize { - self.last_scan_id - } - - fn set_scan_id(&mut self, scan_id: usize) { - self.last_scan_id = scan_id; - } - - fn with_repo(&mut self, _: Box) { - unimplemented!(); - } -} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6fd00aabbd84c37855e9dae788cf85499b31e8af..ee54fdb394155afa2ffb2ade5dd4cb6fced269df 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -100,7 +100,7 @@ pub struct Snapshot { pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories: Vec>, + git_repositories: Vec, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, @@ -115,7 +115,7 @@ impl Clone for LocalSnapshot { git_repositories: self .git_repositories .iter() - .map(|repo| repo.boxed_clone()) + .map(|repo| repo.clone()) .collect(), removed_entry_ids: self.removed_entry_ids.clone(), next_entry_id: self.next_entry_id.clone(), @@ -157,7 +157,7 @@ struct ShareState { pub enum Event { UpdatedEntries, - UpdatedGitRepositories(Vec>), + UpdatedGitRepositories(Vec), } impl Entity for Worktree { @@ -1307,53 +1307,35 @@ impl LocalSnapshot { } // Gives the most specific git repository for a given path - pub(crate) fn git_repository_for_file_path( - &self, - path: &Path, - ) -> Option> { - let repos = self - .git_repositories - .iter() - .map(|repo| repo.content_path().to_str().unwrap().to_string()) - .collect::>(); - dbg!(repos); - + pub(crate) fn git_repository_for_file_path(&self, path: &Path) -> Option { self.git_repositories .iter() .rev() //git_repository is ordered lexicographically - .find(|repo| repo.is_path_managed_by(path)) - .map(|repo| repo.boxed_clone()) + .find(|repo| { + repo.is_path_managed_by(&self.abs_path.join(path)) + }) + .map(|repo| repo.clone()) } // ~/zed: // - src // - crates // - .git -> /usr/.git - pub(crate) fn git_repository_for_git_data( - &self, - path: &Path, - ) -> Option> { + pub(crate) fn git_repository_for_git_data(&self, path: &Path) -> Option { self.git_repositories .iter() - .find(|repo| repo.is_path_in_git_folder(path)) - .map(|repo| repo.boxed_clone()) + .find(|repo| repo.is_path_in_git_folder(&self.abs_path.join(path))) + .map(|repo| repo.clone()) } pub(crate) fn does_git_repository_track_file_path( &self, - repo: &Box, + repo: &GitRepository, file_path: &Path, ) -> bool { - // /zed - // - .git - // - a.txt - // - /dep - // - b.txt - // - .git - // Depends on git_repository_for_file_path returning the most specific git repository for a given path - self.git_repository_for_file_path(file_path) - .map_or(false, |r| &r == repo) + self.git_repository_for_file_path(&self.abs_path.join(file_path)) + .map_or(false, |r| r.git_dir_path() == repo.git_dir_path()) } #[cfg(test)] @@ -1438,7 +1420,6 @@ impl LocalSnapshot { } fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { - dbg!(&entry.path); if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { @@ -1456,19 +1437,6 @@ impl LocalSnapshot { ); } } - } else if entry.path.file_name() == Some(&DOT_GIT) { - dbg!(&entry.path); - - let abs_path = self.abs_path.join(&entry.path); - let content_path: Arc = entry.path.parent().unwrap().into(); - if let Err(ix) = self - .git_repositories - .binary_search_by_key(&content_path.as_ref(), |repo| repo.content_path()) - { - if let Some(repository) = fs.open_git_repository(&abs_path, &content_path) { - self.git_repositories.insert(ix, repository); - } - } } self.reuse_entry_id(&mut entry); @@ -1506,6 +1474,7 @@ impl LocalSnapshot { parent_path: Arc, entries: impl IntoIterator, ignore: Option>, + fs: &dyn Fs, ) { let mut parent_entry = if let Some(parent_entry) = self.entries_by_path.get(&PathKey(parent_path.clone()), &()) @@ -1531,6 +1500,18 @@ impl LocalSnapshot { unreachable!(); } + if parent_path.file_name() == Some(&DOT_GIT) { + let abs_path = self.abs_path.join(&parent_path); + if let Err(ix) = self + .git_repositories + .binary_search_by_key(&abs_path.as_path(), |repo| repo.git_dir_path()) + { + if let Some(repository) = fs.open_git_repository(&abs_path) { + self.git_repositories.insert(ix, repository); + } + } + } + let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); @@ -2227,7 +2208,6 @@ impl BackgroundScanner { if ignore_stack.is_all() { if let Some(mut root_entry) = snapshot.root_entry().cloned() { root_entry.is_ignored = true; - dbg!("scan dirs entry"); snapshot.insert_entry(root_entry, self.fs.as_ref()); } } @@ -2375,9 +2355,12 @@ impl BackgroundScanner { new_entries.push(child_entry); } - self.snapshot - .lock() - .populate_dir(job.path.clone(), new_entries, new_ignore); + self.snapshot.lock().populate_dir( + job.path.clone(), + new_entries, + new_ignore, + self.fs.as_ref(), + ); for new_job in new_jobs { job.scan_queue.send(new_job).await.unwrap(); } @@ -2450,7 +2433,6 @@ impl BackgroundScanner { snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); - dbg!("process_events entry"); snapshot.insert_entry(fs_entry, self.fs.as_ref()); let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -3189,8 +3171,6 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - dbg!(tree); - assert!(tree .git_repository_for_file_path("c.txt".as_ref()) .is_none()); @@ -3202,22 +3182,22 @@ mod tests { // Need to update the file system for anything involving git // Goal: Make this test pass // Up Next: Invalidating git repos! - assert_eq!(repo.content_path(), Path::new("dir1")); - assert_eq!(repo.git_dir_path(), Path::new("dir1/.git")); + assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap()); + assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap()); let repo = tree .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!(repo.content_path(), Path::new("dir1/deps/dep1")); - assert_eq!(repo.git_dir_path(), Path::new("dir1/deps/dep1")); + assert_eq!(repo.content_path(), root.path().join("dir1/deps/dep1").canonicalize().unwrap()); + assert_eq!(repo.git_dir_path(), root.path().join("dir1/deps/dep1/.git").canonicalize().unwrap()); let repo = tree .git_repository_for_git_data("dir1/.git/HEAD".as_ref()) .unwrap(); - assert_eq!(repo.content_path(), Path::new("dir1")); - assert_eq!(repo.git_dir_path(), Path::new("dir1/.git")); + assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap()); + assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap()); assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref())); assert!(!tree From d2b18790a0d7b9d597d3015380ea82e66229086c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 27 Sep 2022 14:07:53 -0400 Subject: [PATCH 34/73] Remove git repos from worktree when deleted on storage Co-Authored-By: Mikayla Maki --- crates/project/src/git_repository.rs | 2 +- crates/project/src/worktree.rs | 68 ++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index d1df841fe71d4e55d55d37731b25167a428ce42b..73f7130e56da9fbcfc2f2210a70f90ecf1f87f46 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -56,7 +56,7 @@ impl GitRepository { self.last_scan_id = scan_id; } - pub fn with_repo(&mut self, f: Box) { + pub fn with_repo(&mut self, f: F) { let mut git2 = self.libgit_repository.lock(); f(&mut git2) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ee54fdb394155afa2ffb2ade5dd4cb6fced269df..a9ebfd8612944b7f2315bf98b4d0719f0041be07 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1311,9 +1311,7 @@ impl LocalSnapshot { self.git_repositories .iter() .rev() //git_repository is ordered lexicographically - .find(|repo| { - repo.is_path_managed_by(&self.abs_path.join(path)) - }) + .find(|repo| repo.is_path_managed_by(&self.abs_path.join(path))) .map(|repo| repo.clone()) } @@ -2548,13 +2546,16 @@ impl BackgroundScanner { } async fn update_git_repositories(&self) { - let mut snapshot = self.snapshot(); - let mut git_repositories = mem::take(&mut snapshot.git_repositories); - git_repositories.retain(|git_repository| { - let dot_git_path = git_repository.content_path().join(&*DOT_GIT); - snapshot.entry_for_path(dot_git_path).is_some() - }); - snapshot.git_repositories = git_repositories; + let mut snapshot = self.snapshot.lock(); + + let new_repos = snapshot + .git_repositories + .iter() + .cloned() + .filter(|repo| git2::Repository::open(repo.git_dir_path()).is_ok()) + .collect(); + + snapshot.git_repositories = new_repos; } async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { @@ -3179,30 +3180,59 @@ mod tests { .git_repository_for_file_path("dir1/src/b.txt".as_ref()) .unwrap(); - // Need to update the file system for anything involving git - // Goal: Make this test pass - // Up Next: Invalidating git repos! - assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap()); - assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap()); + assert_eq!( + repo.content_path(), + root.path().join("dir1").canonicalize().unwrap() + ); + assert_eq!( + repo.git_dir_path(), + root.path().join("dir1/.git").canonicalize().unwrap() + ); let repo = tree .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!(repo.content_path(), root.path().join("dir1/deps/dep1").canonicalize().unwrap()); - assert_eq!(repo.git_dir_path(), root.path().join("dir1/deps/dep1/.git").canonicalize().unwrap()); + assert_eq!( + repo.content_path(), + root.path().join("dir1/deps/dep1").canonicalize().unwrap() + ); + assert_eq!( + repo.git_dir_path(), + root.path() + .join("dir1/deps/dep1/.git") + .canonicalize() + .unwrap() + ); let repo = tree .git_repository_for_git_data("dir1/.git/HEAD".as_ref()) .unwrap(); - assert_eq!(repo.content_path(), root.path().join("dir1").canonicalize().unwrap()); - assert_eq!(repo.git_dir_path(), root.path().join("dir1/.git").canonicalize().unwrap()); + assert_eq!( + repo.content_path(), + root.path().join("dir1").canonicalize().unwrap() + ); + assert_eq!( + repo.git_dir_path(), + root.path().join("dir1/.git").canonicalize().unwrap() + ); assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref())); assert!(!tree .does_git_repository_track_file_path(&repo, "dir1/deps/dep1/src/a.txt".as_ref())); }); + + std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree + .git_repository_for_file_path("dir1/src/b.txt".as_ref()) + .is_none()); + }); } #[gpui::test] From 759b7f1e07257b431c048d381e26b858b92933ce Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 27 Sep 2022 14:37:33 -0400 Subject: [PATCH 35/73] Update repo scan id when files under dot git dir events Co-Authored-By: Mikayla Maki --- crates/project/src/git_repository.rs | 16 +++--- crates/project/src/worktree.rs | 74 ++++++++++++---------------- 2 files changed, 39 insertions(+), 51 deletions(-) diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index 73f7130e56da9fbcfc2f2210a70f90ecf1f87f46..eab031da17c0e8c84464c676de46d2d03eaeadf2 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -10,7 +10,7 @@ pub struct GitRepository { // Path to the actual .git folder. // Note: if .git is a file, this points to the folder indicated by the .git file git_dir_path: Arc, - last_scan_id: usize, + scan_id: usize, libgit_repository: Arc>, } @@ -22,19 +22,19 @@ impl GitRepository { Some(Self { content_path: libgit_repository.workdir()?.into(), git_dir_path: dotgit_path.canonicalize().log_err()?.into(), - last_scan_id: 0, + scan_id: 0, libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)), }) }) } - pub fn is_path_managed_by(&self, path: &Path) -> bool { + pub fn manages(&self, path: &Path) -> bool { path.canonicalize() .map(|path| path.starts_with(&self.content_path)) .unwrap_or(false) } - pub fn is_path_in_git_folder(&self, path: &Path) -> bool { + pub fn in_dot_git(&self, path: &Path) -> bool { path.canonicalize() .map(|path| path.starts_with(&self.git_dir_path)) .unwrap_or(false) @@ -48,12 +48,12 @@ impl GitRepository { self.git_dir_path.as_ref() } - pub fn last_scan_id(&self) -> usize { - self.last_scan_id + pub fn scan_id(&self) -> usize { + self.scan_id } - pub fn set_scan_id(&mut self, scan_id: usize) { - self.last_scan_id = scan_id; + pub(super) fn set_scan_id(&mut self, scan_id: usize) { + self.scan_id = scan_id; } pub fn with_repo(&mut self, f: F) { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a9ebfd8612944b7f2315bf98b4d0719f0041be07..aead63102b218cd376612ac9942998a39e083453 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -40,7 +40,6 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, DerefMut}, os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, @@ -1307,32 +1306,24 @@ impl LocalSnapshot { } // Gives the most specific git repository for a given path - pub(crate) fn git_repository_for_file_path(&self, path: &Path) -> Option { + pub(crate) fn repo_for(&self, path: &Path) -> Option { self.git_repositories .iter() .rev() //git_repository is ordered lexicographically - .find(|repo| repo.is_path_managed_by(&self.abs_path.join(path))) + .find(|repo| repo.manages(&self.abs_path.join(path))) .map(|repo| repo.clone()) } - // ~/zed: - // - src - // - crates - // - .git -> /usr/.git - pub(crate) fn git_repository_for_git_data(&self, path: &Path) -> Option { + pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepository> { self.git_repositories - .iter() - .find(|repo| repo.is_path_in_git_folder(&self.abs_path.join(path))) - .map(|repo| repo.clone()) + .iter_mut() + .rev() //git_repository is ordered lexicographically + .find(|repo| repo.in_dot_git(&self.abs_path.join(path))) } - pub(crate) fn does_git_repository_track_file_path( - &self, - repo: &GitRepository, - file_path: &Path, - ) -> bool { + pub(crate) fn tracks_filepath(&self, repo: &GitRepository, file_path: &Path) -> bool { // Depends on git_repository_for_file_path returning the most specific git repository for a given path - self.git_repository_for_file_path(&self.abs_path.join(file_path)) + self.repo_for(&self.abs_path.join(file_path)) .map_or(false, |r| r.git_dir_path() == repo.git_dir_path()) } @@ -2433,6 +2424,11 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); + let scan_id = snapshot.scan_id; + if let Some(repo) = snapshot.in_dot_git(&abs_path) { + repo.set_scan_id(scan_id); + } + let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { ancestor_inodes.insert(metadata.inode); @@ -3172,13 +3168,9 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree - .git_repository_for_file_path("c.txt".as_ref()) - .is_none()); + assert!(tree.repo_for("c.txt".as_ref()).is_none()); - let repo = tree - .git_repository_for_file_path("dir1/src/b.txt".as_ref()) - .unwrap(); + let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); assert_eq!( repo.content_path(), @@ -3189,9 +3181,7 @@ mod tests { root.path().join("dir1/.git").canonicalize().unwrap() ); - let repo = tree - .git_repository_for_file_path("dir1/deps/dep1/src/a.txt".as_ref()) - .unwrap(); + let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); assert_eq!( repo.content_path(), @@ -3204,23 +3194,23 @@ mod tests { .canonicalize() .unwrap() ); + }); - let repo = tree - .git_repository_for_git_data("dir1/.git/HEAD".as_ref()) - .unwrap(); + let original_scan_id = tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id() + }); - assert_eq!( - repo.content_path(), - root.path().join("dir1").canonicalize().unwrap() - ); - assert_eq!( - repo.git_dir_path(), - root.path().join("dir1/.git").canonicalize().unwrap() - ); + std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); + tree.flush_fs_events(cx).await; - assert!(tree.does_git_repository_track_file_path(&repo, "dir1/src/b.txt".as_ref())); - assert!(!tree - .does_git_repository_track_file_path(&repo, "dir1/deps/dep1/src/a.txt".as_ref())); + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id(); + assert_ne!( + original_scan_id, new_scan_id, + "original {original_scan_id}, new {new_scan_id}" + ); }); std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); @@ -3229,9 +3219,7 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - assert!(tree - .git_repository_for_file_path("dir1/src/b.txt".as_ref()) - .is_none()); + assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none()); }); } From 7e5d49487be16511f1246048b221997e9956d8f2 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 27 Sep 2022 20:06:18 -0400 Subject: [PATCH 36/73] WIP Notifying buffers of head text change Co-Authored-By: Mikayla Maki --- crates/editor/src/items.rs | 4 +- crates/editor/src/multi_buffer.rs | 6 +-- crates/language/src/buffer.rs | 14 +++++-- crates/project/src/fs.rs | 46 ---------------------- crates/project/src/git_repository.rs | 44 +++++++++++++++++++-- crates/project/src/project.rs | 29 +++++++++++++- crates/project/src/worktree.rs | 57 +++++++++++++++++++++++++--- crates/workspace/src/workspace.rs | 12 +++--- 8 files changed, 142 insertions(+), 70 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 76e148018089913944fe0ca5379ce76f23658d28..c1082020e5f254cec81946cb62ec372f9651bd0d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -478,13 +478,13 @@ impl Item for Editor { }) } - fn update_git( + fn git_diff_recalc( &mut self, _project: ModelHandle, cx: &mut ViewContext, ) -> Task> { self.buffer().update(cx, |multibuffer, cx| { - multibuffer.update_git(cx); + multibuffer.git_diff_recalc(cx); }); Task::ready(Ok(())) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 2f93bc5b0930f9cd118dbe90a06f9c62b8668ba3..76093e0496c817d4ed1ce1b7d47ffe4366ae3e89 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -312,13 +312,13 @@ impl MultiBuffer { self.read(cx).symbols_containing(offset, theme) } - pub fn update_git(&mut self, cx: &mut ModelContext) { + pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { let buffers = self.buffers.borrow(); for buffer_state in buffers.values() { - if buffer_state.buffer.read(cx).needs_git_update() { + if buffer_state.buffer.read(cx).needs_git_diff_recalc() { buffer_state .buffer - .update(cx, |buffer, cx| buffer.update_git(cx)) + .update(cx, |buffer, cx| buffer.git_diff_recalc(cx)) } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6ecfbc7e6293f75bad776817dbf5fc1c25f92ba8..d1dfb9ec22581f1e4390cff082adca2222edd1f5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -613,6 +613,7 @@ impl Buffer { cx, ); } + self.update_git(cx); cx.emit(Event::Reloaded); cx.notify(); } @@ -661,12 +662,19 @@ impl Buffer { self.file = Some(new_file); task } + + pub fn update_git(&mut self, cx: &mut ModelContext) { + //Grab head text + + + self.git_diff_recalc(cx); + } - pub fn needs_git_update(&self) -> bool { + pub fn needs_git_diff_recalc(&self) -> bool { self.git_diff_status.diff.needs_update(self) } - pub fn update_git(&mut self, cx: &mut ModelContext) { + pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; return; @@ -692,7 +700,7 @@ impl Buffer { this.git_diff_status.update_in_progress = false; if this.git_diff_status.update_requested { - this.update_git(cx); + this.git_diff_recalc(cx); } }) } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index e675ddf8e58f67218c6526af73db9cd2acd4cd76..8542030cb7d646ee71d5afa81a1a27a09f8d36de 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -34,7 +34,6 @@ 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; @@ -48,7 +47,6 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; - fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; @@ -168,38 +166,6 @@ 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?; @@ -274,10 +240,6 @@ impl Fs for RealFs { }))) } - fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option { - GitRepository::open(abs_dotgit_path) - } - fn is_fake(&self) -> bool { false } @@ -791,10 +753,6 @@ 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); @@ -893,10 +851,6 @@ impl Fs for FakeFs { })) } - fn open_git_repository(&self, _: &Path) -> Option { - None - } - fn is_fake(&self) -> bool { true } diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index eab031da17c0e8c84464c676de46d2d03eaeadf2..c27b1ba3859152996ae40d5f1da3f6cd8667408e 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -1,6 +1,7 @@ -use git2::Repository; +use anyhow::Result; +use git2::{Repository as LibGitRepository, RepositoryOpenFlags as LibGitRepositoryOpenFlags}; use parking_lot::Mutex; -use std::{path::Path, sync::Arc}; +use std::{path::Path, sync::Arc, ffi::OsStr}; use util::ResultExt; #[derive(Clone)] @@ -11,12 +12,12 @@ pub struct GitRepository { // Note: if .git is a file, this points to the folder indicated by the .git file git_dir_path: Arc, scan_id: usize, - libgit_repository: Arc>, + libgit_repository: Arc>, } impl GitRepository { pub fn open(dotgit_path: &Path) -> Option { - Repository::open(&dotgit_path) + LibGitRepository::open(&dotgit_path) .log_err() .and_then(|libgit_repository| { Some(Self { @@ -60,4 +61,39 @@ impl GitRepository { let mut git2 = self.libgit_repository.lock(); f(&mut git2) } + + pub async fn load_head_text(&self, file_path: &Path) -> Option { + fn logic(repo: &LibGitRepository, file_path: &Path) -> Result> { + let object = repo + .head()? + .peel_to_tree()? + .get_path(file_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(&self.libgit_repository.lock(), file_path) { + Ok(value) => return value, + Err(err) => log::error!("Error loading head text: {:?}", err), + } + None + } +} + +impl std::fmt::Debug for GitRepository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GitRepository") + .field("content_path", &self.content_path) + .field("git_dir_path", &self.git_dir_path) + .field("scan_id", &self.scan_id) + .field("libgit_repository", &"LibGitRepository") + .finish() + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 78a500585a3f2bbf997a5c4bdd98c05c5cc04446..4aa3a89d860fc77df056b7120103904e07c0a5bb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -13,6 +13,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; +use git_repository::GitRepository; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, @@ -4536,7 +4537,9 @@ impl Project { if worktree.read(cx).is_local() { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), - worktree::Event::UpdatedGitRepositories(_) => todo!(), + worktree::Event::UpdatedGitRepositories(updated_repos) => { + this.update_local_worktree_buffers_git_repos(updated_repos, cx) + } }) .detach(); } @@ -4644,6 +4647,30 @@ impl Project { } } + fn update_local_worktree_buffers_git_repos( + &mut self, + updated_repos: &[GitRepository], + cx: &mut ModelContext, + ) { + for (buffer_id, buffer) in &self.opened_buffers { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| { + let updated = updated_repos.iter().any(|repo| { + buffer + .file() + .and_then(|file| file.as_local()) + .map(|file| repo.manages(&file.abs_path(cx))) + .unwrap_or(false) + }); + + if updated { + buffer.update_git(cx); + } + }); + } + } + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index aead63102b218cd376612ac9942998a39e083453..beef854470ba5a808f129173d8f046270f8c9c72 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -467,7 +467,7 @@ impl LocalWorktree { .await?; Ok(cx.add_model(|cx| { let mut buffer = Buffer::from_file(0, contents, head_text, Arc::new(file), cx); - buffer.update_git(cx); + buffer.git_diff_recalc(cx); buffer })) }) @@ -522,16 +522,28 @@ impl LocalWorktree { match self.scan_state() { ScanState::Idle => { - self.snapshot = self.background_snapshot.lock().clone(); + let new_snapshot = self.background_snapshot.lock().clone(); + let updated_repos = self.list_updated_repos(&new_snapshot); + self.snapshot = new_snapshot; + if let Some(share) = self.share.as_mut() { *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); } + cx.emit(Event::UpdatedEntries); + + if !updated_repos.is_empty() { + cx.emit(Event::UpdatedGitRepositories(updated_repos)); + } } ScanState::Initializing => { let is_fake_fs = self.fs.is_fake(); - self.snapshot = self.background_snapshot.lock().clone(); + + let new_snapshot = self.background_snapshot.lock().clone(); + let updated_repos = self.list_updated_repos(&new_snapshot); + self.snapshot = new_snapshot; + self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { if is_fake_fs { #[cfg(any(test, feature = "test-support"))] @@ -543,7 +555,12 @@ impl LocalWorktree { this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); } })); + cx.emit(Event::UpdatedEntries); + + if !updated_repos.is_empty() { + cx.emit(Event::UpdatedGitRepositories(updated_repos)); + } } _ => { @@ -556,6 +573,34 @@ impl LocalWorktree { cx.notify(); } + fn list_updated_repos(&self, new_snapshot: &LocalSnapshot) -> Vec { + let old_snapshot = &self.snapshot; + + fn diff<'a>( + a: &'a LocalSnapshot, + b: &'a LocalSnapshot, + updated: &mut HashMap<&'a Path, GitRepository>, + ) { + for a_repo in &a.git_repositories { + let matched = b.git_repositories.iter().find(|b_repo| { + a_repo.git_dir_path() == b_repo.git_dir_path() + && a_repo.scan_id() == b_repo.scan_id() + }); + + if matched.is_some() { + updated.insert(a_repo.git_dir_path(), a_repo.clone()); + } + } + } + + let mut updated = HashMap::<&Path, GitRepository>::default(); + + diff(old_snapshot, new_snapshot, &mut updated); + diff(new_snapshot, old_snapshot, &mut updated); + + updated.into_values().collect() + } + pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.last_scan_state_rx.clone(); async move { @@ -606,9 +651,11 @@ impl LocalWorktree { files_included, settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked ) { + + let fs = fs.clone(); let abs_path = abs_path.clone(); - let task = async move { fs.load_head_text(&abs_path).await }; + let opt_future = async move { fs.load_head_text(&abs_path).await }; let results = cx.background().spawn(task).await; if files_included == settings::GitFilesIncluded::All { @@ -1495,7 +1542,7 @@ impl LocalSnapshot { .git_repositories .binary_search_by_key(&abs_path.as_path(), |repo| repo.git_dir_path()) { - if let Some(repository) = fs.open_git_repository(&abs_path) { + if let Some(repository) = GitRepository::open(&abs_path) { self.git_repositories.insert(ix, repository); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9e8338d2896294237642af9c8facf564c9af8d54..921fb2de201e4c1235a47239543de5d6f0d8bd71 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -317,7 +317,7 @@ pub trait Item: View { project: ModelHandle, cx: &mut ViewContext, ) -> Task>; - fn update_git( + fn git_diff_recalc( &mut self, _project: ModelHandle, _cx: &mut ViewContext, @@ -539,7 +539,7 @@ pub trait ItemHandle: 'static + fmt::Debug { ) -> Task>; fn reload(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task>; - fn update_git( + fn git_diff_recalc( &self, project: ModelHandle, cx: &mut MutableAppContext, @@ -753,7 +753,7 @@ impl ItemHandle for ViewHandle { workspace, cx, |project, mut cx| async move { - cx.update(|cx| item.update_git(project, cx)) + cx.update(|cx| item.git_diff_recalc(project, cx)) .await .log_err(); }, @@ -762,7 +762,7 @@ impl ItemHandle for ViewHandle { let project = workspace.project().downgrade(); cx.spawn_weak(|_, mut cx| async move { if let Some(project) = project.upgrade(&cx) { - cx.update(|cx| item.update_git(project, cx)) + cx.update(|cx| item.git_diff_recalc(project, cx)) .await .log_err(); } @@ -850,12 +850,12 @@ impl ItemHandle for ViewHandle { self.update(cx, |item, cx| item.reload(project, cx)) } - fn update_git( + fn git_diff_recalc( &self, project: ModelHandle, cx: &mut MutableAppContext, ) -> Task> { - self.update(cx, |item, cx| item.update_git(project, cx)) + self.update(cx, |item, cx| item.git_diff_recalc(project, cx)) } fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { From bf3b3da6edbf654dbda2e0d4f553aa8460da92a2 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 28 Sep 2022 10:26:30 -0400 Subject: [PATCH 37/73] Build again --- crates/language/src/buffer.rs | 3 +-- crates/project/src/git_repository.rs | 2 +- crates/project/src/worktree.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d1dfb9ec22581f1e4390cff082adca2222edd1f5..9d386c14adf3001ba7f769dd4054f843f66a0cf4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -662,10 +662,9 @@ impl Buffer { self.file = Some(new_file); task } - + pub fn update_git(&mut self, cx: &mut ModelContext) { //Grab head text - self.git_diff_recalc(cx); } diff --git a/crates/project/src/git_repository.rs b/crates/project/src/git_repository.rs index c27b1ba3859152996ae40d5f1da3f6cd8667408e..4b46b1839183a0cfdcd4013733929136aee116f6 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/project/src/git_repository.rs @@ -1,7 +1,7 @@ use anyhow::Result; use git2::{Repository as LibGitRepository, RepositoryOpenFlags as LibGitRepositoryOpenFlags}; use parking_lot::Mutex; -use std::{path::Path, sync::Arc, ffi::OsStr}; +use std::{ffi::OsStr, path::Path, sync::Arc}; use util::ResultExt; #[derive(Clone)] diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index beef854470ba5a808f129173d8f046270f8c9c72..4885ce104a767a03f3cb7c3e6d2f0ba249a6745f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -636,6 +636,7 @@ impl LocalWorktree { let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); + let snapshot = self.snapshot(); let files_included = cx .global::() @@ -651,12 +652,11 @@ impl LocalWorktree { files_included, settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked ) { - - - let fs = fs.clone(); - let abs_path = abs_path.clone(); - let opt_future = async move { fs.load_head_text(&abs_path).await }; - let results = cx.background().spawn(task).await; + let results = if let Some(repo) = snapshot.repo_for(&abs_path) { + repo.load_head_text(&abs_path).await + } else { + None + }; if files_included == settings::GitFilesIncluded::All { results.or_else(|| Some(text.clone())) From d5fd531743680c568e64faa75e1059f20b215453 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 28 Sep 2022 11:43:33 -0400 Subject: [PATCH 38/73] Move git related things into specialized git crate Co-Authored-By: Mikayla Maki --- Cargo.lock | 22 +++++++- crates/editor/Cargo.toml | 1 + crates/editor/src/element.rs | 2 +- crates/editor/src/multi_buffer.rs | 8 +-- crates/git/Cargo.toml | 22 ++++++++ .../{language/src/git.rs => git/src/diff.rs} | 6 +-- crates/git/src/git.rs | 12 +++++ .../src/repository.rs} | 20 +++---- crates/language/Cargo.toml | 2 +- crates/language/src/buffer.rs | 33 ++++++------ crates/language/src/language.rs | 1 - crates/project/Cargo.toml | 3 +- crates/project/src/fs.rs | 4 -- crates/project/src/project.rs | 43 +++++++++------ crates/project/src/worktree.rs | 54 +++++++++++-------- crates/util/src/lib.rs | 7 --- crates/util/src/test.rs | 9 ++-- 17 files changed, 150 insertions(+), 99 deletions(-) create mode 100644 crates/git/Cargo.toml rename crates/{language/src/git.rs => git/src/diff.rs} (98%) create mode 100644 crates/git/src/git.rs rename crates/{project/src/git_repository.rs => git/src/repository.rs} (80%) diff --git a/Cargo.lock b/Cargo.lock index 8157327cf225ce1d4bdf33facc293b18da51bfba..c8918158be51d4a8e94683dd339a207e83436e81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1697,6 +1697,7 @@ dependencies = [ "env_logger", "futures", "fuzzy", + "git", "gpui", "indoc", "itertools", @@ -2224,6 +2225,23 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "git" +version = "0.1.0" +dependencies = [ + "anyhow", + "clock", + "git2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "smol", + "sum_tree", + "text", + "unindent", + "util", +] + [[package]] name = "git2" version = "0.15.0" @@ -2853,7 +2871,7 @@ dependencies = [ "env_logger", "futures", "fuzzy", - "git2", + "git", "gpui", "lazy_static", "log", @@ -3996,7 +4014,7 @@ dependencies = [ "fsevent", "futures", "fuzzy", - "git2", + "git", "gpui", "ignore", "language", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index dfd4938742d3a365d477c01c8d54e2ce9bdb2e9b..2ea7473b59c1026a8c1bc01519da67e6ba91e1fb 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -25,6 +25,7 @@ clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } fuzzy = { path = "../fuzzy" } +git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2e767d72e67cc9880227b8d0c29c59f1d58ee432..4bc9f9a10be24274d3c72b21304d5c743c86b717 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,6 +16,7 @@ use crate::{ }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; +use git::diff::{DiffHunk, DiffHunkStatus}; use gpui::{ color::Color, elements::*, @@ -34,7 +35,6 @@ use gpui::{ WeakViewHandle, }; use json::json; -use language::git::{DiffHunk, DiffHunkStatus}; use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; use settings::Settings; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 76093e0496c817d4ed1ce1b7d47ffe4366ae3e89..b4e302e3c30659ce5cd4b6adee68df3e7d80a579 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -4,13 +4,13 @@ pub use anchor::{Anchor, AnchorRangeExt}; use anyhow::Result; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; +use git::diff::DiffHunk; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, git::DiffHunk, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, - Chunk, DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, - OutlineItem, Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, - TransactionId, + char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, + DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, + Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; use smallvec::SmallVec; use std::{ diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..79ac56d098d56cb566bd80a70febfffc75367280 --- /dev/null +++ b/crates/git/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "git" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/git.rs" + +[dependencies] +anyhow = "1.0.38" +clock = { path = "../clock" } +git2 = { version = "0.15", default-features = false } +lazy_static = "1.4.0" +sum_tree = { path = "../sum_tree" } +text = { path = "../text" } +util = { path = "../util" } +log = { version = "0.4.16", features = ["kv_unstable_serde"] } +smol = "1.2" +parking_lot = "0.11.1" + +[dev-dependencies] +unindent = "0.1.7" diff --git a/crates/language/src/git.rs b/crates/git/src/diff.rs similarity index 98% rename from crates/language/src/git.rs rename to crates/git/src/diff.rs index d713dcbc144ed8560e38091f8084d3654cac9f4f..ddaddb72890edb94fffe0fc20b7d58cb302feed2 100644 --- a/crates/language/src/git.rs +++ b/crates/git/src/diff.rs @@ -259,7 +259,7 @@ mod tests { use text::Buffer; use unindent::Unindent as _; - #[gpui::test] + #[test] fn test_buffer_diff_simple() { let head_text = " one @@ -308,8 +308,4 @@ mod tests { ); } } - - // use rand::rngs::StdRng; - // #[gpui::test(iterations = 100)] - // fn test_buffer_diff_random(mut rng: StdRng) {} } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..36f54e706a4401b7798bbd27ca3363351428e5c8 --- /dev/null +++ b/crates/git/src/git.rs @@ -0,0 +1,12 @@ +use std::ffi::OsStr; + +pub use git2 as libgit; +pub use lazy_static::lazy_static; + +pub mod diff; +pub mod repository; + +lazy_static! { + pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); + pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} diff --git a/crates/project/src/git_repository.rs b/crates/git/src/repository.rs similarity index 80% rename from crates/project/src/git_repository.rs rename to crates/git/src/repository.rs index 4b46b1839183a0cfdcd4013733929136aee116f6..a38d13ef0d4af7c18609fe53a4429444d5f0686e 100644 --- a/crates/project/src/git_repository.rs +++ b/crates/git/src/repository.rs @@ -1,7 +1,7 @@ use anyhow::Result; -use git2::{Repository as LibGitRepository, RepositoryOpenFlags as LibGitRepositoryOpenFlags}; +use git2::Repository as LibGitRepository; use parking_lot::Mutex; -use std::{ffi::OsStr, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; use util::ResultExt; #[derive(Clone)] @@ -53,21 +53,17 @@ impl GitRepository { self.scan_id } - pub(super) fn set_scan_id(&mut self, scan_id: usize) { + pub fn set_scan_id(&mut self, scan_id: usize) { + println!("setting scan id"); self.scan_id = scan_id; } - pub fn with_repo(&mut self, f: F) { - let mut git2 = self.libgit_repository.lock(); - f(&mut git2) - } - - pub async fn load_head_text(&self, file_path: &Path) -> Option { - fn logic(repo: &LibGitRepository, file_path: &Path) -> Result> { + pub async fn load_head_text(&self, relative_file_path: &Path) -> Option { + fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { let object = repo .head()? .peel_to_tree()? - .get_path(file_path)? + .get_path(relative_file_path)? .to_object(&repo)?; let content = match object.as_blob() { @@ -79,7 +75,7 @@ impl GitRepository { Ok(Some(head_text)) } - match logic(&self.libgit_repository.lock(), file_path) { + match logic(&self.libgit_repository.as_ref().lock(), relative_file_path) { Ok(value) => return value, Err(err) => log::error!("Error loading head text: {:?}", err), } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 034b10e89c64d201dbff0ab3409900ba9558c2a8..7a218acc8e59f3c655962668e1ef7e9e67de17a5 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -25,6 +25,7 @@ client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } +git = { path = "../git" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } @@ -51,7 +52,6 @@ smol = "1.2" tree-sitter = "0.20" tree-sitter-rust = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true } -git2 = { version = "0.15", default-features = false } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9d386c14adf3001ba7f769dd4054f843f66a0cf4..13fe6daed5ddcf844703d7a4c49b92c23b20bc4b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,4 +1,3 @@ -use crate::git; pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, @@ -47,14 +46,14 @@ pub use {tree_sitter_rust, tree_sitter_typescript}; pub use lsp::DiagnosticSeverity; struct GitDiffStatus { - diff: git::BufferDiff, + diff: git::diff::BufferDiff, update_in_progress: bool, update_requested: bool, } pub struct Buffer { text: TextBuffer, - head_text: Option>, + head_text: Option, git_diff_status: GitDiffStatus, file: Option>, saved_version: clock::Global, @@ -83,7 +82,7 @@ pub struct Buffer { pub struct BufferSnapshot { text: text::BufferSnapshot, - pub git_diff: git::BufferDiff, + pub git_diff: git::diff::BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: DiagnosticSet, @@ -353,7 +352,7 @@ impl Buffer { ) -> Self { Self::build( TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), - head_text.map(|h| Arc::new(h.into())), + head_text.map(|h| h.into().into_boxed_str().into()), Some(file), ) } @@ -364,7 +363,11 @@ impl Buffer { file: Option>, ) -> Result { let buffer = TextBuffer::new(replica_id, message.id, message.base_text); - let mut this = Self::build(buffer, message.head_text.map(|text| Arc::new(text)), file); + let mut this = Self::build( + buffer, + message.head_text.map(|text| text.into_boxed_str().into()), + file, + ); this.text.set_line_ending(proto::deserialize_line_ending( proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, @@ -420,11 +423,7 @@ impl Buffer { self } - fn build( - buffer: TextBuffer, - head_text: Option>, - 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 { @@ -440,7 +439,7 @@ impl Buffer { text: buffer, head_text, git_diff_status: GitDiffStatus { - diff: git::BufferDiff::new(), + diff: git::diff::BufferDiff::new(), update_in_progress: false, update_requested: false, }, @@ -613,7 +612,7 @@ impl Buffer { cx, ); } - self.update_git(cx); + self.git_diff_recalc(cx); cx.emit(Event::Reloaded); cx.notify(); } @@ -663,9 +662,8 @@ impl Buffer { task } - pub fn update_git(&mut self, cx: &mut ModelContext) { - //Grab head text - + pub fn update_head_text(&mut self, head_text: Option, cx: &mut ModelContext) { + self.head_text = head_text; self.git_diff_recalc(cx); } @@ -674,6 +672,7 @@ impl Buffer { } pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { + println!("recalc"); if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; return; @@ -2221,7 +2220,7 @@ impl BufferSnapshot { pub fn git_diff_hunks_in_range<'a>( &'a self, query_row_range: Range, - ) -> impl 'a + Iterator> { + ) -> impl 'a + Iterator> { self.git_diff.hunks_in_range(query_row_range, self) } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8e2fe601e7bf105056699e18018fb5f8246fa1eb..780f6e75b52521bf751f7de1a748b91954001ac3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,6 +1,5 @@ mod buffer; mod diagnostic_set; -pub mod git; mod highlight_map; mod outline; pub mod proto; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 8ca01eac2c55389bbfa04c57fed0cdfa179c9a8e..1e45e3c6ed9d654ee720f3cc3d6e93fa9ced0dad 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -24,6 +24,7 @@ collections = { path = "../collections" } db = { path = "../db" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } +git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } @@ -52,8 +53,6 @@ smol = "1.2.5" thiserror = "1.0.29" toml = "0.5" rocksdb = "0.18" -git2 = { version = "0.15", default-features = false } - [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 8542030cb7d646ee71d5afa81a1a27a09f8d36de..6a496910a0c3c95125d2b73a62a16837c45c2c13 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,11 +1,9 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use language::git::libgit::{Repository, RepositoryOpenFlags}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ - ffi::OsStr, io, os::unix::fs::MetadataExt, path::{Component, Path, PathBuf}, @@ -22,8 +20,6 @@ use futures::lock::Mutex; #[cfg(any(test, feature = "test-support"))] use std::sync::{Arc, Weak}; -use crate::git_repository::GitRepository; - #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4aa3a89d860fc77df056b7120103904e07c0a5bb..57af588c6844a5ec9eee9aa63a3dd830d78d7745 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,4 @@ pub mod fs; -mod git_repository; mod ignore; mod lsp_command; pub mod search; @@ -13,7 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; -use git_repository::GitRepository; +use git::repository::GitRepository; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, @@ -4538,6 +4537,7 @@ impl Project { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), worktree::Event::UpdatedGitRepositories(updated_repos) => { + println!("{updated_repos:#?}"); this.update_local_worktree_buffers_git_repos(updated_repos, cx) } }) @@ -4649,24 +4649,35 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, - updated_repos: &[GitRepository], + repos: &[GitRepository], cx: &mut ModelContext, ) { - for (buffer_id, buffer) in &self.opened_buffers { + //TODO: Produce protos + + for (_, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| { - let updated = updated_repos.iter().any(|repo| { - buffer - .file() - .and_then(|file| file.as_local()) - .map(|file| repo.manages(&file.abs_path(cx))) - .unwrap_or(false) - }); + let file = match buffer.read(cx).file().and_then(|file| file.as_local()) { + Some(file) => file, + None => return, + }; + let path = file.path().clone(); + let abs_path = file.abs_path(cx); + println!("got file"); - if updated { - buffer.update_git(cx); - } - }); + let repo = match repos.iter().find(|repo| repo.manages(&abs_path)) { + Some(repo) => repo.clone(), + None => return, + }; + println!("got repo"); + + cx.spawn(|_, mut cx| async move { + let head_text = repo.load_head_text(&path).await; + buffer.update(&mut cx, |buffer, cx| { + println!("got calling update"); + buffer.update_head_text(head_text, cx); + }); + }) + .detach(); } } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 4885ce104a767a03f3cb7c3e6d2f0ba249a6745f..7fd37dc0162ad5f23b768c4cd10939916dbb69fb 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,10 +1,9 @@ -use crate::{copy_recursive, git_repository::GitRepository, ProjectEntryId, RemoveOptions}; - use super::{ fs::{self, Fs}, ignore::IgnoreStack, DiagnosticSummary, }; +use crate::{copy_recursive, ProjectEntryId, RemoveOptions}; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; @@ -18,6 +17,8 @@ use futures::{ Stream, StreamExt, }; use fuzzy::CharBag; +use git::repository::GitRepository; +use git::{DOT_GIT, GITIGNORE}; use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, @@ -48,7 +49,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{ResultExt, TryFutureExt, DOT_GIT, GITIGNORE}; +use util::{ResultExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -523,7 +524,10 @@ impl LocalWorktree { match self.scan_state() { ScanState::Idle => { let new_snapshot = self.background_snapshot.lock().clone(); - let updated_repos = self.list_updated_repos(&new_snapshot); + let updated_repos = Self::list_updated_repos( + &self.snapshot.git_repositories, + &new_snapshot.git_repositories, + ); self.snapshot = new_snapshot; if let Some(share) = self.share.as_mut() { @@ -541,7 +545,10 @@ impl LocalWorktree { let is_fake_fs = self.fs.is_fake(); let new_snapshot = self.background_snapshot.lock().clone(); - let updated_repos = self.list_updated_repos(&new_snapshot); + let updated_repos = Self::list_updated_repos( + &self.snapshot.git_repositories, + &new_snapshot.git_repositories, + ); self.snapshot = new_snapshot; self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { @@ -573,16 +580,20 @@ impl LocalWorktree { cx.notify(); } - fn list_updated_repos(&self, new_snapshot: &LocalSnapshot) -> Vec { - let old_snapshot = &self.snapshot; + fn list_updated_repos( + old_repos: &[GitRepository], + new_repos: &[GitRepository], + ) -> Vec { + println!("old repos: {:#?}", old_repos); + println!("new repos: {:#?}", new_repos); fn diff<'a>( - a: &'a LocalSnapshot, - b: &'a LocalSnapshot, + a: &'a [GitRepository], + b: &'a [GitRepository], updated: &mut HashMap<&'a Path, GitRepository>, ) { - for a_repo in &a.git_repositories { - let matched = b.git_repositories.iter().find(|b_repo| { + for a_repo in a { + let matched = b.iter().find(|b_repo| { a_repo.git_dir_path() == b_repo.git_dir_path() && a_repo.scan_id() == b_repo.scan_id() }); @@ -595,10 +606,10 @@ impl LocalWorktree { let mut updated = HashMap::<&Path, GitRepository>::default(); - diff(old_snapshot, new_snapshot, &mut updated); - diff(new_snapshot, old_snapshot, &mut updated); + diff(old_repos, new_repos, &mut updated); + diff(new_repos, old_repos, &mut updated); - updated.into_values().collect() + dbg!(updated.into_values().collect()) } pub fn scan_complete(&self) -> impl Future { @@ -653,7 +664,7 @@ impl LocalWorktree { settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked ) { let results = if let Some(repo) = snapshot.repo_for(&abs_path) { - repo.load_head_text(&abs_path).await + repo.load_head_text(&path).await } else { None }; @@ -1362,6 +1373,7 @@ impl LocalSnapshot { } pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepository> { + println!("chechking {path:?}"); self.git_repositories .iter_mut() .rev() //git_repository is ordered lexicographically @@ -1510,7 +1522,6 @@ impl LocalSnapshot { parent_path: Arc, entries: impl IntoIterator, ignore: Option>, - fs: &dyn Fs, ) { let mut parent_entry = if let Some(parent_entry) = self.entries_by_path.get(&PathKey(parent_path.clone()), &()) @@ -2391,12 +2402,9 @@ impl BackgroundScanner { new_entries.push(child_entry); } - self.snapshot.lock().populate_dir( - job.path.clone(), - new_entries, - new_ignore, - self.fs.as_ref(), - ); + self.snapshot + .lock() + .populate_dir(job.path.clone(), new_entries, new_ignore); for new_job in new_jobs { job.scan_queue.send(new_job).await.unwrap(); } @@ -2595,7 +2603,7 @@ impl BackgroundScanner { .git_repositories .iter() .cloned() - .filter(|repo| git2::Repository::open(repo.git_dir_path()).is_ok()) + .filter(|repo| git::libgit::Repository::open(repo.git_dir_path()).is_ok()) .collect(); snapshot.git_repositories = new_repos; diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 52bf70e3a7167a926b2aecdda9eb3c6e14f6858f..97f409f410c36c2b2de7fb14e9cc7ef94f4996b1 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -2,20 +2,13 @@ pub mod test; use futures::Future; -use lazy_static::lazy_static; use std::{ cmp::Ordering, - ffi::OsStr, ops::AddAssign, pin::Pin, task::{Context, Poll}, }; -lazy_static! { - pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); - pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); -} - pub fn truncate(s: &str, max_chars: usize) -> &str { match s.char_indices().nth(max_chars) { None => s, diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 4e4716434e28a105f43f8a9248f9a848f52ef74e..96d13f4c8158278d2b9e625827d820d39a03c866 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -2,14 +2,15 @@ mod assertions; mod marked_text; use git2; -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; use tempdir::TempDir; pub use assertions::*; pub use marked_text::*; -use crate::DOT_GIT; - pub fn temp_tree(tree: serde_json::Value) -> TempDir { let dir = TempDir::new("").unwrap(); write_tree(dir.path(), tree); @@ -28,7 +29,7 @@ fn write_tree(path: &Path, tree: serde_json::Value) { Value::Object(_) => { fs::create_dir(&path).unwrap(); - if path.file_name() == Some(&DOT_GIT) { + if path.file_name() == Some(&OsStr::new(".git")) { git2::Repository::init(&path.parent().unwrap()).unwrap(); } From 71b2126eca51cd3a5c9796a86aad6800a33e9184 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 28 Sep 2022 11:42:22 -0700 Subject: [PATCH 39/73] WIP, re-doing fs and fake git repos --- Cargo.lock | 1 + crates/git/Cargo.toml | 2 + crates/git/src/repository.rs | 123 ++++++++++++++++++++++++++++----- crates/language/src/buffer.rs | 1 - crates/project/src/fs.rs | 10 +++ crates/project/src/project.rs | 8 +-- crates/project/src/worktree.rs | 50 +++++++------- 7 files changed, 145 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8918158be51d4a8e94683dd339a207e83436e81..3c87f336de5ff49ce91fcb9c1e03b978b5a8c1b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2230,6 +2230,7 @@ name = "git" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "clock", "git2", "lazy_static", diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 79ac56d098d56cb566bd80a70febfffc75367280..7ef9a953ba52e631af5fe7ce25bad2f5a1715d38 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -17,6 +17,8 @@ util = { path = "../util" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } smol = "1.2" parking_lot = "0.11.1" +async-trait = "0.1" + [dev-dependencies] unindent = "0.1.7" diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a38d13ef0d4af7c18609fe53a4429444d5f0686e..19ba0d1238ce6e66f9c3494047e166bb44eb4a4e 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -4,8 +4,29 @@ use parking_lot::Mutex; use std::{path::Path, sync::Arc}; use util::ResultExt; +#[async_trait::async_trait] +pub trait GitRepository: Send + Sync + std::fmt::Debug { + fn manages(&self, path: &Path) -> bool; + + fn in_dot_git(&self, path: &Path) -> bool; + + fn content_path(&self) -> &Path; + + fn git_dir_path(&self) -> &Path; + + fn scan_id(&self) -> usize; + + fn set_scan_id(&mut self, scan_id: usize); + + fn git_repo(&self) -> Arc>; + + fn boxed_clone(&self) -> Box; + + async fn load_head_text(&self, relative_file_path: &Path) -> Option; +} + #[derive(Clone)] -pub struct GitRepository { +pub struct RealGitRepository { // Path to folder containing the .git file or directory content_path: Arc, // Path to the actual .git folder. @@ -15,50 +36,48 @@ pub struct GitRepository { libgit_repository: Arc>, } -impl GitRepository { - pub fn open(dotgit_path: &Path) -> Option { +impl RealGitRepository { + pub fn open(dotgit_path: &Path) -> Option> { LibGitRepository::open(&dotgit_path) .log_err() - .and_then(|libgit_repository| { - Some(Self { + .and_then::, _>(|libgit_repository| { + Some(Box::new(Self { content_path: libgit_repository.workdir()?.into(), git_dir_path: dotgit_path.canonicalize().log_err()?.into(), scan_id: 0, libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)), - }) + })) }) } +} - pub fn manages(&self, path: &Path) -> bool { +#[async_trait::async_trait] +impl GitRepository for RealGitRepository { + fn manages(&self, path: &Path) -> bool { path.canonicalize() .map(|path| path.starts_with(&self.content_path)) .unwrap_or(false) } - pub fn in_dot_git(&self, path: &Path) -> bool { + fn in_dot_git(&self, path: &Path) -> bool { path.canonicalize() .map(|path| path.starts_with(&self.git_dir_path)) .unwrap_or(false) } - pub fn content_path(&self) -> &Path { + fn content_path(&self) -> &Path { self.content_path.as_ref() } - pub fn git_dir_path(&self) -> &Path { + fn git_dir_path(&self) -> &Path { self.git_dir_path.as_ref() } - pub fn scan_id(&self) -> usize { + fn scan_id(&self) -> usize { self.scan_id } - pub fn set_scan_id(&mut self, scan_id: usize) { - println!("setting scan id"); - self.scan_id = scan_id; - } - - pub async fn load_head_text(&self, relative_file_path: &Path) -> Option { + async fn load_head_text(&self, relative_file_path: &Path) -> Option { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { let object = repo .head()? @@ -81,9 +100,21 @@ impl GitRepository { } None } + + fn git_repo(&self) -> Arc> { + self.libgit_repository.clone() + } + + fn set_scan_id(&mut self, scan_id: usize) { + self.scan_id = scan_id; + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } } -impl std::fmt::Debug for GitRepository { +impl std::fmt::Debug for RealGitRepository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GitRepository") .field("content_path", &self.content_path) @@ -93,3 +124,59 @@ impl std::fmt::Debug for GitRepository { .finish() } } + +#[derive(Debug, Clone)] +pub struct FakeGitRepository { + content_path: Arc, + git_dir_path: Arc, + scan_id: usize, +} + +impl FakeGitRepository { + pub fn open(dotgit_path: &Path, scan_id: usize) -> Box { + Box::new(FakeGitRepository { + content_path: dotgit_path.parent().unwrap().into(), + git_dir_path: dotgit_path.into(), + scan_id, + }) + } +} + +#[async_trait::async_trait] +impl GitRepository for FakeGitRepository { + fn manages(&self, path: &Path) -> bool { + path.starts_with(self.content_path()) + } + + fn in_dot_git(&self, path: &Path) -> bool { + path.starts_with(self.git_dir_path()) + } + + fn content_path(&self) -> &Path { + &self.content_path + } + + fn git_dir_path(&self) -> &Path { + &self.git_dir_path + } + + fn scan_id(&self) -> usize { + self.scan_id + } + + async fn load_head_text(&self, _: &Path) -> Option { + unimplemented!() + } + + fn git_repo(&self) -> Arc> { + unimplemented!() + } + + fn set_scan_id(&mut self, scan_id: usize) { + self.scan_id = scan_id; + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 13fe6daed5ddcf844703d7a4c49b92c23b20bc4b..0268f1cc68f40ff44a5a20a3c0a0d32870bbf134 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -672,7 +672,6 @@ impl Buffer { } pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { - println!("recalc"); if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; return; diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 6a496910a0c3c95125d2b73a62a16837c45c2c13..4b27a23856b8bea1a5dfd40d4b07a014ffc320b9 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; +use git::repository::{FakeGitRepository, GitRepository, RealGitRepository}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ @@ -43,6 +44,7 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; + async fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; @@ -236,6 +238,10 @@ impl Fs for RealFs { }))) } + fn open_repo(&self, abs_dot_git: &Path) -> Option> { + RealGitRepository::open(&abs_dot_git) + } + fn is_fake(&self) -> bool { false } @@ -847,6 +853,10 @@ impl Fs for FakeFs { })) } + fn open_repo(&self, abs_dot_git: &Path) -> Option> { + Some(FakeGitRepository::open(abs_dot_git.into(), 0)) + } + fn is_fake(&self) -> bool { true } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 57af588c6844a5ec9eee9aa63a3dd830d78d7745..a2a49c9c9306cf04a6801aa882c820f24917bd98 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4537,7 +4537,6 @@ impl Project { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), worktree::Event::UpdatedGitRepositories(updated_repos) => { - println!("{updated_repos:#?}"); this.update_local_worktree_buffers_git_repos(updated_repos, cx) } }) @@ -4649,7 +4648,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, - repos: &[GitRepository], + repos: &[Box], cx: &mut ModelContext, ) { //TODO: Produce protos @@ -4662,18 +4661,15 @@ impl Project { }; let path = file.path().clone(); let abs_path = file.abs_path(cx); - println!("got file"); let repo = match repos.iter().find(|repo| repo.manages(&abs_path)) { - Some(repo) => repo.clone(), + Some(repo) => repo.boxed_clone(), None => return, }; - println!("got repo"); cx.spawn(|_, mut cx| async move { let head_text = repo.load_head_text(&path).await; buffer.update(&mut cx, |buffer, cx| { - println!("got calling update"); buffer.update_head_text(head_text, cx); }); }) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7fd37dc0162ad5f23b768c4cd10939916dbb69fb..ae55659f985630f72bc43bbaa270b0e422c4483a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -100,7 +100,7 @@ pub struct Snapshot { pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories: Vec, + git_repositories: Vec>, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, @@ -115,7 +115,7 @@ impl Clone for LocalSnapshot { git_repositories: self .git_repositories .iter() - .map(|repo| repo.clone()) + .map(|repo| repo.boxed_clone()) .collect(), removed_entry_ids: self.removed_entry_ids.clone(), next_entry_id: self.next_entry_id.clone(), @@ -157,7 +157,7 @@ struct ShareState { pub enum Event { UpdatedEntries, - UpdatedGitRepositories(Vec), + UpdatedGitRepositories(Vec>), } impl Entity for Worktree { @@ -581,16 +581,13 @@ impl LocalWorktree { } fn list_updated_repos( - old_repos: &[GitRepository], - new_repos: &[GitRepository], - ) -> Vec { - println!("old repos: {:#?}", old_repos); - println!("new repos: {:#?}", new_repos); - + old_repos: &[Box], + new_repos: &[Box], + ) -> Vec> { fn diff<'a>( - a: &'a [GitRepository], - b: &'a [GitRepository], - updated: &mut HashMap<&'a Path, GitRepository>, + a: &'a [Box], + b: &'a [Box], + updated: &mut HashMap<&'a Path, Box>, ) { for a_repo in a { let matched = b.iter().find(|b_repo| { @@ -599,17 +596,17 @@ impl LocalWorktree { }); if matched.is_some() { - updated.insert(a_repo.git_dir_path(), a_repo.clone()); + updated.insert(a_repo.git_dir_path(), a_repo.boxed_clone()); } } } - let mut updated = HashMap::<&Path, GitRepository>::default(); + let mut updated = HashMap::<&Path, Box>::default(); diff(old_repos, new_repos, &mut updated); diff(new_repos, old_repos, &mut updated); - dbg!(updated.into_values().collect()) + updated.into_values().collect() } pub fn scan_complete(&self) -> impl Future { @@ -1364,23 +1361,22 @@ impl LocalSnapshot { } // Gives the most specific git repository for a given path - pub(crate) fn repo_for(&self, path: &Path) -> Option { + pub(crate) fn repo_for(&self, path: &Path) -> Option> { self.git_repositories .iter() .rev() //git_repository is ordered lexicographically .find(|repo| repo.manages(&self.abs_path.join(path))) - .map(|repo| repo.clone()) + .map(|repo| repo.boxed_clone()) } - pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepository> { - println!("chechking {path:?}"); + pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut Box> { self.git_repositories .iter_mut() .rev() //git_repository is ordered lexicographically .find(|repo| repo.in_dot_git(&self.abs_path.join(path))) } - pub(crate) fn tracks_filepath(&self, repo: &GitRepository, file_path: &Path) -> bool { + pub(crate) fn _tracks_filepath(&self, repo: &dyn GitRepository, file_path: &Path) -> bool { // Depends on git_repository_for_file_path returning the most specific git repository for a given path self.repo_for(&self.abs_path.join(file_path)) .map_or(false, |r| r.git_dir_path() == repo.git_dir_path()) @@ -1522,6 +1518,7 @@ impl LocalSnapshot { parent_path: Arc, entries: impl IntoIterator, ignore: Option>, + fs: &dyn Fs, ) { let mut parent_entry = if let Some(parent_entry) = self.entries_by_path.get(&PathKey(parent_path.clone()), &()) @@ -1553,7 +1550,7 @@ impl LocalSnapshot { .git_repositories .binary_search_by_key(&abs_path.as_path(), |repo| repo.git_dir_path()) { - if let Some(repository) = GitRepository::open(&abs_path) { + if let Some(repository) = fs.open_repo(abs_path.as_path()) { self.git_repositories.insert(ix, repository); } } @@ -2402,9 +2399,12 @@ impl BackgroundScanner { new_entries.push(child_entry); } - self.snapshot - .lock() - .populate_dir(job.path.clone(), new_entries, new_ignore); + self.snapshot.lock().populate_dir( + job.path.clone(), + new_entries, + new_ignore, + self.fs.as_ref(), + ); for new_job in new_jobs { job.scan_queue.send(new_job).await.unwrap(); } @@ -2602,7 +2602,7 @@ impl BackgroundScanner { let new_repos = snapshot .git_repositories .iter() - .cloned() + .map(|repo| repo.boxed_clone()) .filter(|repo| git::libgit::Repository::open(repo.git_dir_path()).is_ok()) .collect(); From f7714a25d1556fe3ac3efa49cc4abbd883fb24b7 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 28 Sep 2022 16:52:24 -0400 Subject: [PATCH 40/73] Don't pretend this is async --- crates/project/src/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 4b27a23856b8bea1a5dfd40d4b07a014ffc320b9..cc1f6101f4c5604c6185397e3e7f4f74a86457a9 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -44,7 +44,7 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; - async fn open_repo(&self, abs_dot_git: &Path) -> Option>; + fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; From 113d3b88d0e1aeb31d8488fe1296bc563c4d842e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 28 Sep 2022 14:16:21 -0700 Subject: [PATCH 41/73] Added test, and fix, for changed_repos method on LocalWorktree --- crates/project/src/worktree.rs | 52 +++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ae55659f985630f72bc43bbaa270b0e422c4483a..81eec4987fbfd5e8b9aaccfadf038f1f48018fc6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -524,7 +524,7 @@ impl LocalWorktree { match self.scan_state() { ScanState::Idle => { let new_snapshot = self.background_snapshot.lock().clone(); - let updated_repos = Self::list_updated_repos( + let updated_repos = Self::changed_repos( &self.snapshot.git_repositories, &new_snapshot.git_repositories, ); @@ -545,7 +545,7 @@ impl LocalWorktree { let is_fake_fs = self.fs.is_fake(); let new_snapshot = self.background_snapshot.lock().clone(); - let updated_repos = Self::list_updated_repos( + let updated_repos = Self::changed_repos( &self.snapshot.git_repositories, &new_snapshot.git_repositories, ); @@ -580,7 +580,7 @@ impl LocalWorktree { cx.notify(); } - fn list_updated_repos( + fn changed_repos( old_repos: &[Box], new_repos: &[Box], ) -> Vec> { @@ -595,7 +595,7 @@ impl LocalWorktree { && a_repo.scan_id() == b_repo.scan_id() }); - if matched.is_some() { + if matched.is_none() { updated.insert(a_repo.git_dir_path(), a_repo.boxed_clone()); } } @@ -2955,6 +2955,7 @@ mod tests { use anyhow::Result; use client::test::FakeHttpClient; use fs::RealFs; + use git::repository::FakeGitRepository; use gpui::{executor::Deterministic, TestAppContext}; use rand::prelude::*; use serde_json::json; @@ -3278,6 +3279,49 @@ mod tests { }); } + #[test] + fn test_changed_repos() { + let prev_repos: Vec> = vec![ + FakeGitRepository::open(Path::new("/.git"), 0), + FakeGitRepository::open(Path::new("/a/.git"), 0), + FakeGitRepository::open(Path::new("/a/b/.git"), 0), + ]; + + let new_repos: Vec> = vec![ + FakeGitRepository::open(Path::new("/a/.git"), 1), + FakeGitRepository::open(Path::new("/a/b/.git"), 0), + FakeGitRepository::open(Path::new("/a/c/.git"), 0), + ]; + + let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); + + dbg!(&res); + + // Deletion retained + assert!(res + .iter() + .find(|repo| repo.git_dir_path() == Path::new("/.git") && repo.scan_id() == 0) + .is_some()); + + // Update retained + assert!(res + .iter() + .find(|repo| repo.git_dir_path() == Path::new("/a/.git") && repo.scan_id() == 1) + .is_some()); + + // Addition retained + assert!(res + .iter() + .find(|repo| repo.git_dir_path() == Path::new("/a/c/.git") && repo.scan_id() == 0) + .is_some()); + + // Nochange, not retained + assert!(res + .iter() + .find(|repo| repo.git_dir_path() == Path::new("/a/b/.git") && repo.scan_id() == 0) + .is_none()); + } + #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { let dir = temp_tree(json!({ From 8a2430090b11cb01d6c94d78a9def3f96d8a9a2d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 21 Sep 2022 10:39:03 -0400 Subject: [PATCH 42/73] WIP Git gutter styling --- crates/editor/src/element.rs | 6 +++--- styles/src/styleTree/editor.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4bc9f9a10be24274d3c72b21304d5c743c86b717..82bd260819dc37004cd0dcb9e2f928ed9027c6ad 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -567,7 +567,7 @@ impl EditorElement { let start_y = row as f32 * line_height + offset - scroll_top; let end_y = start_y + line_height; - let width = 0.4 * line_height; + let width = 0.275 * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); @@ -589,7 +589,7 @@ impl EditorElement { let start_y = start_row as f32 * line_height - scroll_top; let end_y = end_row as f32 * line_height - scroll_top; - let width = 0.22 * line_height; + let width = 0.12 * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); @@ -598,7 +598,7 @@ impl EditorElement { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: 0.2 * line_height, + corner_radius: 0.05 * line_height, }); } diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 29d6857964999bfdff0cdbf7f6be78c58c7f5b6b..bd01c3b84576dc1be10cd125a5a8793e60822c40 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -60,9 +60,9 @@ export default function editor(theme: Theme) { indicator: iconColor(theme, "secondary"), verticalScale: 0.618 }, - diffBackgroundDeleted: theme.ramps.red(0.3).hex(), - diffBackgroundInserted: theme.ramps.green(0.3).hex(), - diffBackgroundModified: theme.ramps.blue(0.3).hex(), + diffBackgroundDeleted: theme.iconColor.error, + diffBackgroundInserted: theme.iconColor.ok, + diffBackgroundModified: theme.iconColor.warning, documentHighlightReadBackground: theme.editor.highlight.occurrence, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence, errorColor: theme.textColor.error, From b395fbb3f2214564b1f4e5cf8afcadbbe43748b2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 21 Sep 2022 15:39:51 -0400 Subject: [PATCH 43/73] wip --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 82bd260819dc37004cd0dcb9e2f928ed9027c6ad..b8731f8707dad06524fe46d72e50eb1fe7d06d73 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -589,7 +589,7 @@ impl EditorElement { let start_y = start_row as f32 * line_height - scroll_top; let end_y = end_row as f32 * line_height - scroll_top; - let width = 0.12 * line_height; + let width = 0.16 * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); From 9fe6a5e83e1c6a8cdfbe3669a25576fed0a4dbbe Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 28 Sep 2022 14:58:52 -0700 Subject: [PATCH 44/73] made git stuff slightly more themable --- crates/editor/src/element.rs | 18 ++++++++++++++---- crates/theme/src/theme.rs | 3 +++ styles/src/styleTree/editor.ts | 3 +++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b8731f8707dad06524fe46d72e50eb1fe7d06d73..57ee919288f8fb56fadd5a52bc71ae24a28be301 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -545,12 +545,22 @@ impl EditorElement { } } - let (inserted_color, modified_color, deleted_color) = { + let ( + inserted_color, + modified_color, + deleted_color, + width_multiplier, + corner_radius, + removed_width_mult, + ) = { let editor = &cx.global::().theme.editor; ( editor.diff_background_inserted, editor.diff_background_modified, editor.diff_background_deleted, + editor.diff_indicator_width_multiplier, + editor.diff_indicator_corner_radius, + editor.removed_diff_width_multiplier, ) }; @@ -567,7 +577,7 @@ impl EditorElement { let start_y = row as f32 * line_height + offset - scroll_top; let end_y = start_y + line_height; - let width = 0.275 * line_height; + let width = removed_width_mult * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); @@ -589,7 +599,7 @@ impl EditorElement { let start_y = start_row as f32 * line_height - scroll_top; let end_y = end_row as f32 * line_height - scroll_top; - let width = 0.16 * line_height; + let width = width_multiplier * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); @@ -598,7 +608,7 @@ impl EditorElement { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: 0.05 * line_height, + corner_radius: corner_radius * line_height, }); } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1fd586efeeb0a6b948c04022139400e886c202ab..0d0c94ea8db1f9143efb1abd3e0d68b1019192e2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -491,6 +491,9 @@ pub struct Editor { pub diff_background_deleted: Color, pub diff_background_inserted: Color, pub diff_background_modified: Color, + pub removed_diff_width_multiplier: f32, + pub diff_indicator_width_multiplier: f32, + pub diff_indicator_corner_radius: f32, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index bd01c3b84576dc1be10cd125a5a8793e60822c40..6e52c620ee474ddc1f328b42323205a7478b4928 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -63,6 +63,9 @@ export default function editor(theme: Theme) { diffBackgroundDeleted: theme.iconColor.error, diffBackgroundInserted: theme.iconColor.ok, diffBackgroundModified: theme.iconColor.warning, + removedDiffWidthMultiplier: 0.275, + diffIndicatorWidthMultiplier: 0.16, + diffIndicatorCornerRadius: 0.05, documentHighlightReadBackground: theme.editor.highlight.occurrence, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence, errorColor: theme.textColor.error, From e865b85d9cd88001624d3e869e16fc8600067f07 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 29 Sep 2022 13:10:39 -0400 Subject: [PATCH 45/73] Track index instead of head for diffs --- crates/git/src/repository.rs | 32 +++++++++++++++++++++++--------- crates/project/src/fs.rs | 7 +++++-- crates/project/src/worktree.rs | 8 +++++++- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 19ba0d1238ce6e66f9c3494047e166bb44eb4a4e..37b79fa10d40fd4a4397fc0e17b9ac8c1ccf151b 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -18,6 +18,8 @@ pub trait GitRepository: Send + Sync + std::fmt::Debug { fn set_scan_id(&mut self, scan_id: usize); + fn reopen_git_repo(&mut self) -> bool; + fn git_repo(&self) -> Arc>; fn boxed_clone(&self) -> Box; @@ -79,18 +81,15 @@ impl GitRepository for RealGitRepository { async fn load_head_text(&self, relative_file_path: &Path) -> Option { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { - let object = repo - .head()? - .peel_to_tree()? - .get_path(relative_file_path)? - .to_object(&repo)?; - - let content = match object.as_blob() { - Some(blob) => blob.content().to_owned(), + const STAGE_NORMAL: i32 = 0; + let index = repo.index()?; + let oid = match index.get_path(relative_file_path, STAGE_NORMAL) { + Some(entry) => entry.id, None => return Ok(None), }; - let head_text = String::from_utf8(content.to_owned())?; + let content = repo.find_blob(oid)?.content().to_owned(); + let head_text = String::from_utf8(content)?; Ok(Some(head_text)) } @@ -101,6 +100,17 @@ impl GitRepository for RealGitRepository { None } + fn reopen_git_repo(&mut self) -> bool { + match LibGitRepository::open(&self.git_dir_path) { + Ok(repo) => { + self.libgit_repository = Arc::new(Mutex::new(repo)); + true + } + + Err(_) => false, + } + } + fn git_repo(&self) -> Arc> { self.libgit_repository.clone() } @@ -168,6 +178,10 @@ impl GitRepository for FakeGitRepository { unimplemented!() } + fn reopen_git_repo(&mut self) -> bool { + unimplemented!() + } + fn git_repo(&self) -> Arc> { unimplemented!() } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index cc1f6101f4c5604c6185397e3e7f4f74a86457a9..c14edcd5e43b083cd633069a82b4a67276fea99e 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{FakeGitRepository, GitRepository, RealGitRepository}; +use git::repository::{GitRepository, RealGitRepository}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ @@ -854,7 +854,10 @@ impl Fs for FakeFs { } fn open_repo(&self, abs_dot_git: &Path) -> Option> { - Some(FakeGitRepository::open(abs_dot_git.into(), 0)) + Some(git::repository::FakeGitRepository::open( + abs_dot_git.into(), + 0, + )) } fn is_fake(&self) -> bool { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 81eec4987fbfd5e8b9aaccfadf038f1f48018fc6..1d47c843c5248acbb8ced2540c75b2e46b01fe18 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2603,7 +2603,13 @@ impl BackgroundScanner { .git_repositories .iter() .map(|repo| repo.boxed_clone()) - .filter(|repo| git::libgit::Repository::open(repo.git_dir_path()).is_ok()) + .filter_map(|mut repo| { + if repo.reopen_git_repo() { + Some(repo) + } else { + None + } + }) .collect(); snapshot.git_repositories = new_repos; From fcf11b118109a90bcb2167ec309e0afa6d9878f0 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 29 Sep 2022 13:12:49 -0400 Subject: [PATCH 46/73] Bump protocol version to be ahead of main --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b9f6e6a7390a759b4317ed53bd7309d47a9e37b3..2c28462ee3fd7fa67d322fb262c865ae0394af53 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 32; +pub const PROTOCOL_VERSION: u32 = 33; From bce25918a039fe1e7706258b5c60e23a5e6368a2 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 11:13:22 -0400 Subject: [PATCH 47/73] Fix test build --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1d47c843c5248acbb8ced2540c75b2e46b01fe18..0d2594475ce7d0a4ad1547625112177c9a510cc4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3211,7 +3211,7 @@ mod tests { })); let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client); + let client = cx.read(|cx| Client::new(http_client, cx)); let tree = Worktree::local( client, root.path(), From 1c5d15b85e785f0f87a50df14160295e3109185f Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 13:32:54 -0400 Subject: [PATCH 48/73] Use sumtree instead of iterator linear search for diff hunks in range Co-Authored-By: Max Brunsfeld Co-Authored-By: Mikayla Maki --- crates/git/src/diff.rs | 206 +++++++++++++++++++++++--------------- crates/text/src/anchor.rs | 2 +- 2 files changed, 126 insertions(+), 82 deletions(-) diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index ddaddb72890edb94fffe0fc20b7d58cb302feed2..4d12ca90d10219b46ec7c6210322e85d140a5151 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -1,7 +1,7 @@ use std::ops::Range; use sum_tree::SumTree; -use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, ToPoint}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; @@ -37,7 +37,6 @@ impl sum_tree::Item for DiffHunk { fn summary(&self) -> Self::Summary { DiffHunkSummary { buffer_range: self.buffer_range.clone(), - head_range: self.head_byte_range.clone(), } } } @@ -45,54 +44,17 @@ impl sum_tree::Item for DiffHunk { #[derive(Debug, Default, Clone)] pub struct DiffHunkSummary { buffer_range: Range, - head_range: Range, } impl sum_tree::Summary for DiffHunkSummary { type Context = text::BufferSnapshot; - fn add_summary(&mut self, other: &Self, _: &Self::Context) { - self.head_range.start = self.head_range.start.min(other.head_range.start); - self.head_range.end = self.head_range.end.max(other.head_range.end); - } -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct HunkHeadEnd(usize); - -impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkHeadEnd { - fn add_summary(&mut self, summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) { - self.0 = summary.head_range.end; - } - - fn from_summary(summary: &'a DiffHunkSummary, _: &text::BufferSnapshot) -> Self { - HunkHeadEnd(summary.head_range.end) - } -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct HunkBufferStart(u32); - -impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferStart { - fn add_summary(&mut self, summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) { - self.0 = summary.buffer_range.start.to_point(buffer).row; - } - - fn from_summary(summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) -> Self { - HunkBufferStart(summary.buffer_range.start.to_point(buffer).row) - } -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct HunkBufferEnd(u32); - -impl<'a> sum_tree::Dimension<'a, DiffHunkSummary> for HunkBufferEnd { - fn add_summary(&mut self, summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) { - self.0 = summary.buffer_range.end.to_point(buffer).row; - } - - fn from_summary(summary: &'a DiffHunkSummary, buffer: &text::BufferSnapshot) -> Self { - HunkBufferEnd(summary.buffer_range.end.to_point(buffer).row) + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { + self.buffer_range.start = self + .buffer_range + .start + .min(&other.buffer_range.start, buffer); + self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer); } } @@ -115,23 +77,30 @@ impl BufferDiff { query_row_range: Range, buffer: &'a BufferSnapshot, ) -> impl 'a + Iterator> { - self.tree.iter().filter_map(move |hunk| { - let range = hunk.buffer_range.to_point(&buffer); - - if range.start.row <= query_row_range.end && query_row_range.start <= range.end.row { - let end_row = if range.end.column > 0 { - range.end.row + 1 - } else { - range.end.row - }; - - Some(DiffHunk { - buffer_range: range.start.row..end_row, - head_byte_range: hunk.head_byte_range.clone(), - }) + let start = buffer.anchor_before(Point::new(query_row_range.start, 0)); + let end = buffer.anchor_after(Point::new(query_row_range.end, 0)); + + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt(); + !before_start && !after_end + }); + + std::iter::from_fn(move || { + cursor.next(buffer); + let hunk = cursor.item()?; + + let range = hunk.buffer_range.to_point(buffer); + let end_row = if range.end.column > 0 { + range.end.row + 1 } else { - None - } + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + head_byte_range: hunk.head_byte_range.clone(), + }) }) } @@ -270,7 +239,7 @@ mod tests { let buffer_text = " one - hello + HELLO three " .unindent(); @@ -278,10 +247,78 @@ mod tests { let mut buffer = Buffer::new(0, 0, buffer_text); let mut diff = BufferDiff::new(); smol::block_on(diff.update(&head_text, &buffer)); - assert_hunks(&diff, &buffer, &head_text, &[(1..2, "two\n")]); + assert_hunks( + &diff, + &buffer, + &head_text, + &[(1..2, "two\n", "HELLO\n")], + None, + ); buffer.edit([(0..0, "point five\n")]); - assert_hunks(&diff, &buffer, &head_text, &[(2..3, "two\n")]); + smol::block_on(diff.update(&head_text, &buffer)); + assert_hunks( + &diff, + &buffer, + &head_text, + &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + None, + ); + } + + #[test] + fn test_buffer_diff_range() { + let head_text = " + one + two + three + four + five + six + seven + eight + nine + ten + " + .unindent(); + + let buffer_text = " + A + one + B + two + C + three + HELLO + four + five + SIXTEEN + seven + eight + WORLD + nine + + ten + + " + .unindent(); + + let buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&head_text, &buffer)); + assert_eq!(diff.hunks(&buffer).count(), 8); + + assert_hunks( + &diff, + &buffer, + &head_text, + &[ + (6..7, "", "HELLO\n"), + (9..10, "six\n", "SIXTEEN\n"), + (12..13, "", "WORLD\n"), + ], + Some(7..12), + ); } #[track_caller] @@ -289,23 +326,30 @@ mod tests { diff: &BufferDiff, buffer: &BufferSnapshot, head_text: &str, - expected_hunks: &[(Range, &str)], + expected_hunks: &[(Range, &str, &str)], + range: Option>, ) { - let hunks = diff.hunks(buffer).collect::>(); - assert_eq!( - hunks.len(), - expected_hunks.len(), - "actual hunks are {hunks:#?}" - ); - - let diff_iter = hunks.iter().enumerate(); - for ((index, hunk), (expected_range, expected_str)) in diff_iter.zip(expected_hunks) { - assert_eq!(&hunk.buffer_range, expected_range, "for hunk {index}"); - assert_eq!( - &head_text[hunk.head_byte_range.clone()], - *expected_str, - "for hunk {index}" - ); - } + let actual_hunks = diff + .hunks_in_range(range.unwrap_or(0..u32::MAX), buffer) + .map(|hunk| { + ( + hunk.buffer_range.clone(), + &head_text[hunk.head_byte_range], + buffer + .text_for_range( + Point::new(hunk.buffer_range.start, 0) + ..Point::new(hunk.buffer_range.end, 0), + ) + .collect::(), + ) + }) + .collect::>(); + + let expected_hunks: Vec<_> = expected_hunks + .iter() + .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .collect(); + + assert_eq!(actual_hunks, expected_hunks); } } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 9f70ae1cc7b130c02f0edf01f06002e9f3b8dce1..ab0e1eeabc5091b15e9f6e0f70fcfa577c0c27d4 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -26,7 +26,7 @@ impl Anchor { bias: Bias::Right, buffer_id: None, }; - + pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { let fragment_id_comparison = if self.timestamp == other.timestamp { Ordering::Equal From 6540936970916c98d67e540f692e17615f902f80 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 13:51:54 -0400 Subject: [PATCH 49/73] Fix some panics in tests --- crates/git/src/repository.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 37b79fa10d40fd4a4397fc0e17b9ac8c1ccf151b..f834ebc219c790e8119b384a6568bd6e6f214b61 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -175,11 +175,11 @@ impl GitRepository for FakeGitRepository { } async fn load_head_text(&self, _: &Path) -> Option { - unimplemented!() + None } fn reopen_git_repo(&mut self) -> bool { - unimplemented!() + false } fn git_repo(&self) -> Arc> { From ce7f6dd0829fb183e05708f15f93977d9e9c650c Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 15:50:55 -0400 Subject: [PATCH 50/73] Start a test for remote git data updating Co-Authored-By: Mikayla Maki Co-Authored-By: Max Brunsfeld --- Cargo.lock | 3 + crates/collab/Cargo.toml | 5 +- crates/collab/src/integration_tests.rs | 138 +++++++++++++++++++++++++ crates/git/Cargo.toml | 5 +- crates/git/src/diff.rs | 75 +++++++------- crates/git/src/repository.rs | 26 ++++- crates/language/src/buffer.rs | 9 ++ crates/project/src/fs.rs | 54 +++++++++- crates/project/src/worktree.rs | 12 +-- crates/text/src/anchor.rs | 2 +- 10 files changed, 272 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c87f336de5ff49ce91fcb9c1e03b978b5a8c1b0..75dd5530c9175814d9b101372d76e29818a16a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,6 +1031,7 @@ dependencies = [ "env_logger", "envy", "futures", + "git", "gpui", "hyper", "language", @@ -1061,6 +1062,7 @@ dependencies = [ "tracing", "tracing-log", "tracing-subscriber", + "unindent", "util", "workspace", ] @@ -2232,6 +2234,7 @@ dependencies = [ "anyhow", "async-trait", "clock", + "collections", "git2", "lazy_static", "log", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9b3603e6e43e29acb6071a9c7670d07d412a91ba..47c86e0fe7604abe48004ce9870354ae9fe512d4 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Nathan Sobo "] +authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" @@ -26,6 +26,7 @@ base64 = "0.13" clap = { version = "3.1", features = ["derive"], optional = true } envy = "0.4.2" futures = "0.3" +git = { path = "../git" } hyper = "0.14" lazy_static = "1.4" lipsum = { version = "0.8", optional = true } @@ -65,11 +66,13 @@ project = { path = "../project", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } +git = { path = "../git", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" util = { path = "../util" } lazy_static = "1.4" serde_json = { version = "1.0", features = ["preserve_order"] } +unindent = "0.1" [features] seed-support = ["clap", "lipsum", "reqwest"] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3c9886dc16d9135fb51d3d38fd5c6388f853c691..586d988ef109d70e8d55a624a5e66c9e9a7805a6 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -51,6 +51,7 @@ use std::{ time::Duration, }; use theme::ThemeRegistry; +use unindent::Unindent as _; use workspace::{Item, SplitDirection, ToggleFollow, Workspace}; #[ctor::ctor] @@ -946,6 +947,143 @@ async fn test_propagate_saves_and_fs_changes( .await; } +#[gpui::test(iterations = 10)] +async fn test_git_head_text( + executor: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + executor.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".git": {}, + "a.txt": " + one + two + three + ".unindent(), + }), + ) + .await; + + let head_text = " + one + three + " + .unindent(); + + let new_head_text = " + 1 + two + three + " + .unindent(); + + client_a + .fs + .as_fake() + .set_head_state_for_git_repository( + Path::new("/dir/.git"), + &[(Path::new("a.txt"), head_text.clone())], + ) + .await; + + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + + // Create the buffer + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx)) + .await + .unwrap(); + + // Wait for it to catch up to the new diff + buffer_a + .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff()) + .await; + + // Smoke test diffing + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.head_text(), Some(head_text.as_ref())); + git::diff::assert_hunks( + buffer.snapshot().git_diff_hunks_in_range(0..4), + &buffer, + &head_text, + &[(1..2, "", "two\n")], + ); + }); + + // Create remote buffer + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx)) + .await + .unwrap(); + + //TODO: WAIT FOR REMOTE UPDATES TO FINISH + + // Smoke test diffing + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.head_text(), Some(head_text.as_ref())); + git::diff::assert_hunks( + buffer.snapshot().git_diff_hunks_in_range(0..4), + &buffer, + &head_text, + &[(1..2, "", "two\n")], + ); + }); + + // TODO: Create a dummy file event + client_a + .fs + .as_fake() + .set_head_state_for_git_repository( + Path::new("/dir/.git"), + &[(Path::new("a.txt"), new_head_text.clone())], + ) + .await; + + // TODO: Flush this file event + + // Wait for buffer_a to receive it + buffer_a + .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff()) + .await; + + // Smoke test new diffing + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); + git::diff::assert_hunks( + buffer.snapshot().git_diff_hunks_in_range(0..4), + &buffer, + &head_text, + &[(0..1, "1", "one\n")], + ); + }); + + //TODO: WAIT FOR REMOTE UPDATES TO FINISH on B + + // Smoke test B + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); + git::diff::assert_hunks( + buffer.snapshot().git_diff_hunks_in_range(0..4), + &buffer, + &head_text, + &[(0..1, "1", "one\n")], + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_fs_operations( executor: Arc, diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 7ef9a953ba52e631af5fe7ce25bad2f5a1715d38..744fdc8b9985912ce61dbb60d94d6c88866f6707 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -13,12 +13,15 @@ git2 = { version = "0.15", default-features = false } lazy_static = "1.4.0" sum_tree = { path = "../sum_tree" } text = { path = "../text" } +collections = { path = "../collections" } util = { path = "../util" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } smol = "1.2" parking_lot = "0.11.1" async-trait = "0.1" - [dev-dependencies] unindent = "0.1.7" + +[features] +test-support = [] diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 4d12ca90d10219b46ec7c6210322e85d140a5151..6c904d44d1ee4322a3c62f6ce788d44a799fbf7c 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -222,6 +222,40 @@ impl BufferDiff { } } +/// Range (crossing new lines), old, new +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn assert_hunks( + diff_hunks: Iter, + buffer: &BufferSnapshot, + head_text: &str, + expected_hunks: &[(Range, &str, &str)], +) where + Iter: Iterator>, +{ + let actual_hunks = diff_hunks + .map(|hunk| { + ( + hunk.buffer_range.clone(), + &head_text[hunk.head_byte_range], + buffer + .text_for_range( + Point::new(hunk.buffer_range.start, 0) + ..Point::new(hunk.buffer_range.end, 0), + ) + .collect::(), + ) + }) + .collect::>(); + + let expected_hunks: Vec<_> = expected_hunks + .iter() + .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .collect(); + + assert_eq!(actual_hunks, expected_hunks); +} + #[cfg(test)] mod tests { use super::*; @@ -248,21 +282,19 @@ mod tests { let mut diff = BufferDiff::new(); smol::block_on(diff.update(&head_text, &buffer)); assert_hunks( - &diff, + diff.hunks(&buffer), &buffer, &head_text, &[(1..2, "two\n", "HELLO\n")], - None, ); buffer.edit([(0..0, "point five\n")]); smol::block_on(diff.update(&head_text, &buffer)); assert_hunks( - &diff, + diff.hunks(&buffer), &buffer, &head_text, &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], - None, ); } @@ -309,7 +341,7 @@ mod tests { assert_eq!(diff.hunks(&buffer).count(), 8); assert_hunks( - &diff, + diff.hunks_in_range(7..12, &buffer), &buffer, &head_text, &[ @@ -317,39 +349,6 @@ mod tests { (9..10, "six\n", "SIXTEEN\n"), (12..13, "", "WORLD\n"), ], - Some(7..12), ); } - - #[track_caller] - fn assert_hunks( - diff: &BufferDiff, - buffer: &BufferSnapshot, - head_text: &str, - expected_hunks: &[(Range, &str, &str)], - range: Option>, - ) { - let actual_hunks = diff - .hunks_in_range(range.unwrap_or(0..u32::MAX), buffer) - .map(|hunk| { - ( - hunk.buffer_range.clone(), - &head_text[hunk.head_byte_range], - buffer - .text_for_range( - Point::new(hunk.buffer_range.start, 0) - ..Point::new(hunk.buffer_range.end, 0), - ) - .collect::(), - ) - }) - .collect::>(); - - let expected_hunks: Vec<_> = expected_hunks - .iter() - .map(|(r, s, h)| (r.clone(), *s, h.to_string())) - .collect(); - - assert_eq!(actual_hunks, expected_hunks); - } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index f834ebc219c790e8119b384a6568bd6e6f214b61..fb43e44561ab596847bdf766768070eeb302a86b 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,7 +1,11 @@ use anyhow::Result; +use collections::HashMap; use git2::Repository as LibGitRepository; use parking_lot::Mutex; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; #[async_trait::async_trait] @@ -140,14 +144,25 @@ pub struct FakeGitRepository { content_path: Arc, git_dir_path: Arc, scan_id: usize, + state: Arc>, +} + +#[derive(Debug, Clone, Default)] +pub struct FakeGitRepositoryState { + pub index_contents: HashMap, } impl FakeGitRepository { - pub fn open(dotgit_path: &Path, scan_id: usize) -> Box { + pub fn open( + dotgit_path: &Path, + scan_id: usize, + state: Arc>, + ) -> Box { Box::new(FakeGitRepository { content_path: dotgit_path.parent().unwrap().into(), git_dir_path: dotgit_path.into(), scan_id, + state, }) } } @@ -174,12 +189,13 @@ impl GitRepository for FakeGitRepository { self.scan_id } - async fn load_head_text(&self, _: &Path) -> Option { - None + async fn load_head_text(&self, path: &Path) -> Option { + let state = self.state.lock(); + state.index_contents.get(path).cloned() } fn reopen_git_repo(&mut self) -> bool { - false + true } fn git_repo(&self) -> Arc> { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0268f1cc68f40ff44a5a20a3c0a0d32870bbf134..831236ad5dc27f1738d710a253047433dd9800ff 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -662,6 +662,11 @@ impl Buffer { task } + #[cfg(any(test, feature = "test-support"))] + pub fn head_text(&self) -> Option<&str> { + self.head_text.as_deref() + } + pub fn update_head_text(&mut self, head_text: Option, cx: &mut ModelContext) { self.head_text = head_text; self.git_diff_recalc(cx); @@ -671,6 +676,10 @@ impl Buffer { self.git_diff_status.diff.needs_update(self) } + pub fn is_recalculating_git_diff(&self) -> bool { + self.git_diff_status.update_in_progress + } + pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index c14edcd5e43b083cd633069a82b4a67276fea99e..2b914ae37306e72d467cbe32627c3b657a335940 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{GitRepository, RealGitRepository}; +use git::repository::{FakeGitRepositoryState, GitRepository, RealGitRepository}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ @@ -277,6 +277,7 @@ enum FakeFsEntry { inode: u64, mtime: SystemTime, entries: BTreeMap>>, + git_repo_state: Option>>, }, Symlink { target: PathBuf, @@ -391,6 +392,7 @@ impl FakeFs { inode: 0, mtime: SystemTime::now(), entries: Default::default(), + git_repo_state: None, })), next_inode: 1, event_txs: Default::default(), @@ -480,6 +482,31 @@ impl FakeFs { .boxed() } + pub async fn set_head_state_for_git_repository( + &self, + dot_git: &Path, + head_state: &[(&Path, String)], + ) { + let content_path = dot_git.parent().unwrap(); + let state = self.state.lock().await; + let entry = state.read_path(dot_git).await.unwrap(); + let mut entry = entry.lock().await; + + if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { + let repo_state = git_repo_state.get_or_insert_with(Default::default); + let mut repo_state = repo_state.lock(); + + repo_state.index_contents.clear(); + repo_state.index_contents.extend( + head_state + .iter() + .map(|(path, content)| (content_path.join(path), content.clone())), + ); + } else { + panic!("not a directory"); + } + } + pub async fn files(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); @@ -569,6 +596,7 @@ impl Fs for FakeFs { inode, mtime: SystemTime::now(), entries: Default::default(), + git_repo_state: None, })) }); Ok(()) @@ -854,10 +882,26 @@ impl Fs for FakeFs { } fn open_repo(&self, abs_dot_git: &Path) -> Option> { - Some(git::repository::FakeGitRepository::open( - abs_dot_git.into(), - 0, - )) + let executor = self.executor.upgrade().unwrap(); + executor.block(async move { + let state = self.state.lock().await; + let entry = state.read_path(abs_dot_git).await.unwrap(); + let mut entry = entry.lock().await; + if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { + let state = git_repo_state + .get_or_insert_with(|| { + Arc::new(parking_lot::Mutex::new(FakeGitRepositoryState::default())) + }) + .clone(); + Some(git::repository::FakeGitRepository::open( + abs_dot_git.into(), + 0, + state, + )) + } else { + None + } + }) } fn is_fake(&self) -> bool { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0d2594475ce7d0a4ad1547625112177c9a510cc4..d3a5f710e0a314a3a86c02d52e443c29cd985b41 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3288,15 +3288,15 @@ mod tests { #[test] fn test_changed_repos() { let prev_repos: Vec> = vec![ - FakeGitRepository::open(Path::new("/.git"), 0), - FakeGitRepository::open(Path::new("/a/.git"), 0), - FakeGitRepository::open(Path::new("/a/b/.git"), 0), + FakeGitRepository::open(Path::new("/.git"), 0, Default::default()), + FakeGitRepository::open(Path::new("/a/.git"), 0, Default::default()), + FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), ]; let new_repos: Vec> = vec![ - FakeGitRepository::open(Path::new("/a/.git"), 1), - FakeGitRepository::open(Path::new("/a/b/.git"), 0), - FakeGitRepository::open(Path::new("/a/c/.git"), 0), + FakeGitRepository::open(Path::new("/a/.git"), 1, Default::default()), + FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), + FakeGitRepository::open(Path::new("/a/c/.git"), 0, Default::default()), ]; let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index ab0e1eeabc5091b15e9f6e0f70fcfa577c0c27d4..9f70ae1cc7b130c02f0edf01f06002e9f3b8dce1 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -26,7 +26,7 @@ impl Anchor { bias: Bias::Right, buffer_id: None, }; - + pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { let fragment_id_comparison = if self.timestamp == other.timestamp { Ordering::Equal From 42b7820dbbd1095c4e5f66e6a74d984c7843dbfa Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 18:05:09 -0400 Subject: [PATCH 51/73] Perform git diff on remote buffer open --- crates/project/src/project.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a2a49c9c9306cf04a6801aa882c820f24917bd98..1e8567d4d45d60f4a86e722b228e666dbf8cf133 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5816,7 +5816,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let mut opened_buffer_rx = self.opened_buffer.1.clone(); - cx.spawn(|this, cx| async move { + cx.spawn(|this, mut cx| async move { let buffer = loop { let buffer = this.read_with(&cx, |this, cx| { this.opened_buffers @@ -5834,6 +5834,7 @@ impl Project { .await .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?; }; + buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx)); Ok(buffer) }) } From c95646a298d718096019a120b6f7e4ed890c63ce Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 30 Sep 2022 18:25:25 -0400 Subject: [PATCH 52/73] WIP Start refactoring separation of concerns for repo metadata Co-Authored-By: Max Brunsfeld Co-Authored-By: Mikayla Maki --- crates/collab/src/integration_tests.rs | 12 +- crates/git/src/repository.rs | 149 +++++-------------------- crates/language/src/buffer.rs | 4 - crates/project/src/fs.rs | 5 +- crates/project/src/worktree.rs | 55 ++++++--- 5 files changed, 75 insertions(+), 150 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 586d988ef109d70e8d55a624a5e66c9e9a7805a6..d5a4c56b7d95eae1fbabcc67d6d9326351140e2d 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1008,9 +1008,7 @@ async fn test_git_head_text( .unwrap(); // Wait for it to catch up to the new diff - buffer_a - .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff()) - .await; + executor.run_until_parked(); // Smoke test diffing buffer_a.read_with(cx_a, |buffer, _| { @@ -1029,7 +1027,8 @@ async fn test_git_head_text( .await .unwrap(); - //TODO: WAIT FOR REMOTE UPDATES TO FINISH + // Wait remote buffer to catch up to the new diff + executor.run_until_parked(); // Smoke test diffing buffer_b.read_with(cx_b, |buffer, _| { @@ -1055,9 +1054,7 @@ async fn test_git_head_text( // TODO: Flush this file event // Wait for buffer_a to receive it - buffer_a - .condition(cx_a, |buffer, _| !buffer.is_recalculating_git_diff()) - .await; + executor.run_until_parked(); // Smoke test new diffing buffer_a.read_with(cx_a, |buffer, _| { @@ -1071,6 +1068,7 @@ async fn test_git_head_text( }); //TODO: WAIT FOR REMOTE UPDATES TO FINISH on B + executor.run_until_parked(); // Smoke test B buffer_b.read_with(cx_b, |buffer, _| { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index fb43e44561ab596847bdf766768070eeb302a86b..ba8faa4b2b7ccb0ef68ea223b6e1b34aaac9f8a6 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2,88 +2,39 @@ use anyhow::Result; use collections::HashMap; use git2::Repository as LibGitRepository; use parking_lot::Mutex; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; use util::ResultExt; +use std::{path::{Path, PathBuf}, sync::Arc}; #[async_trait::async_trait] -pub trait GitRepository: Send + Sync + std::fmt::Debug { - fn manages(&self, path: &Path) -> bool; - - fn in_dot_git(&self, path: &Path) -> bool; - - fn content_path(&self) -> &Path; - - fn git_dir_path(&self) -> &Path; - - fn scan_id(&self) -> usize; - - fn set_scan_id(&mut self, scan_id: usize); - - fn reopen_git_repo(&mut self) -> bool; - - fn git_repo(&self) -> Arc>; - - fn boxed_clone(&self) -> Box; - - async fn load_head_text(&self, relative_file_path: &Path) -> Option; -} - -#[derive(Clone)] -pub struct RealGitRepository { - // Path to folder containing the .git file or directory - content_path: Arc, - // Path to the actual .git folder. - // Note: if .git is a file, this points to the folder indicated by the .git file - git_dir_path: Arc, - scan_id: usize, - libgit_repository: Arc>, -} - -impl RealGitRepository { - pub fn open(dotgit_path: &Path) -> Option> { +pub trait GitRepository: Send { + // fn manages(&self, path: &Path) -> bool; + // fn reopen_git_repo(&mut self) -> bool; + // fn git_repo(&self) -> Arc>; + // fn boxed_clone(&self) -> Box; + + fn load_head_text(&self, relative_file_path: &Path) -> Option; + + fn open_real(dotgit_path: &Path) -> Option>> + where Self: Sized + { LibGitRepository::open(&dotgit_path) .log_err() - .and_then::, _>(|libgit_repository| { - Some(Box::new(Self { - content_path: libgit_repository.workdir()?.into(), - git_dir_path: dotgit_path.canonicalize().log_err()?.into(), - scan_id: 0, - libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)), - })) + .and_then::>, _>(|libgit_repository| { + Some(Arc::new(Mutex::new(libgit_repository))) }) } } #[async_trait::async_trait] -impl GitRepository for RealGitRepository { - fn manages(&self, path: &Path) -> bool { - path.canonicalize() - .map(|path| path.starts_with(&self.content_path)) - .unwrap_or(false) - } - - fn in_dot_git(&self, path: &Path) -> bool { - path.canonicalize() - .map(|path| path.starts_with(&self.git_dir_path)) - .unwrap_or(false) - } - - fn content_path(&self) -> &Path { - self.content_path.as_ref() - } - - fn git_dir_path(&self) -> &Path { - self.git_dir_path.as_ref() - } +impl GitRepository for LibGitRepository { + // fn manages(&self, path: &Path) -> bool { + // path.canonicalize() + // .map(|path| path.starts_with(&self.content_path)) + // .unwrap_or(false) + // } - fn scan_id(&self) -> usize { - self.scan_id - } - async fn load_head_text(&self, relative_file_path: &Path) -> Option { + fn load_head_text(&self, relative_file_path: &Path) -> Option { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; @@ -97,53 +48,18 @@ impl GitRepository for RealGitRepository { Ok(Some(head_text)) } - match logic(&self.libgit_repository.as_ref().lock(), relative_file_path) { + match logic(&self, relative_file_path) { Ok(value) => return value, Err(err) => log::error!("Error loading head text: {:?}", err), } None } - - fn reopen_git_repo(&mut self) -> bool { - match LibGitRepository::open(&self.git_dir_path) { - Ok(repo) => { - self.libgit_repository = Arc::new(Mutex::new(repo)); - true - } - - Err(_) => false, - } - } - - fn git_repo(&self) -> Arc> { - self.libgit_repository.clone() - } - - fn set_scan_id(&mut self, scan_id: usize) { - self.scan_id = scan_id; - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } -} - -impl std::fmt::Debug for RealGitRepository { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GitRepository") - .field("content_path", &self.content_path) - .field("git_dir_path", &self.git_dir_path) - .field("scan_id", &self.scan_id) - .field("libgit_repository", &"LibGitRepository") - .finish() - } } #[derive(Debug, Clone)] pub struct FakeGitRepository { content_path: Arc, git_dir_path: Arc, - scan_id: usize, state: Arc>, } @@ -153,15 +69,10 @@ pub struct FakeGitRepositoryState { } impl FakeGitRepository { - pub fn open( - dotgit_path: &Path, - scan_id: usize, - state: Arc>, - ) -> Box { + pub fn open(dotgit_path: &Path, state: Arc>) -> Box { Box::new(FakeGitRepository { content_path: dotgit_path.parent().unwrap().into(), git_dir_path: dotgit_path.into(), - scan_id, state, }) } @@ -173,9 +84,9 @@ impl GitRepository for FakeGitRepository { path.starts_with(self.content_path()) } - fn in_dot_git(&self, path: &Path) -> bool { - path.starts_with(self.git_dir_path()) - } + // fn in_dot_git(&self, path: &Path) -> bool { + // path.starts_with(self.git_dir_path()) + // } fn content_path(&self) -> &Path { &self.content_path @@ -185,10 +96,6 @@ impl GitRepository for FakeGitRepository { &self.git_dir_path } - fn scan_id(&self) -> usize { - self.scan_id - } - async fn load_head_text(&self, path: &Path) -> Option { let state = self.state.lock(); state.index_contents.get(path).cloned() @@ -202,10 +109,6 @@ impl GitRepository for FakeGitRepository { unimplemented!() } - fn set_scan_id(&mut self, scan_id: usize) { - self.scan_id = scan_id; - } - fn boxed_clone(&self) -> Box { Box::new(self.clone()) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 831236ad5dc27f1738d710a253047433dd9800ff..22706ab1b59c04919839746ac6b6565280cbe54e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -676,10 +676,6 @@ impl Buffer { self.git_diff_status.diff.needs_update(self) } - pub fn is_recalculating_git_diff(&self) -> bool { - self.git_diff_status.update_in_progress - } - pub fn git_diff_recalc(&mut self, cx: &mut ModelContext) { if self.git_diff_status.update_in_progress { self.git_diff_status.update_requested = true; diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 2b914ae37306e72d467cbe32627c3b657a335940..1280fcb8bcfc6d16f0fa311f0c6122843472f0a4 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{FakeGitRepositoryState, GitRepository, RealGitRepository}; +use git::repository::{FakeGitRepositoryState, GitRepository, Git2Repo}; use language::LineEnding; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ @@ -239,7 +239,7 @@ impl Fs for RealFs { } fn open_repo(&self, abs_dot_git: &Path) -> Option> { - RealGitRepository::open(&abs_dot_git) + Git2Repo::open(&abs_dot_git) } fn is_fake(&self) -> bool { @@ -895,7 +895,6 @@ impl Fs for FakeFs { .clone(); Some(git::repository::FakeGitRepository::open( abs_dot_git.into(), - 0, state, )) } else { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d3a5f710e0a314a3a86c02d52e443c29cd985b41..4f14ab6ad121417b2eb3d40e2f17cb58f69048a4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -97,10 +97,39 @@ pub struct Snapshot { is_complete: bool, } +#[derive(Clone)] +struct GitRepositoryEntry { + repo: Arc>, + + // repo: Box, + scan_id: usize, + // Path to folder containing the .git file or directory + content_path: Arc, + // Path to the actual .git folder. + // Note: if .git is a file, this points to the folder indicated by the .git file + git_dir_path: Arc, +} + +impl std::fmt::Debug for GitRepositoryEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GitRepositoryEntry") + .field("content_path", &self.content_path) + .field("git_dir_path", &self.git_dir_path) + .field("libgit_repository", &"LibGitRepository") + .finish() + } +} + +// impl Clone for GitRepositoryEntry { +// fn clone(&self) -> Self { +// GitRepositoryEntry { repo: self.repo.boxed_clone(), scan_id: self.scan_id } +// } +// } + pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - git_repositories: Vec>, + git_repositories: Vec, removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, @@ -115,7 +144,7 @@ impl Clone for LocalSnapshot { git_repositories: self .git_repositories .iter() - .map(|repo| repo.boxed_clone()) + .cloned() .collect(), removed_entry_ids: self.removed_entry_ids.clone(), next_entry_id: self.next_entry_id.clone(), @@ -3287,17 +3316,17 @@ mod tests { #[test] fn test_changed_repos() { - let prev_repos: Vec> = vec![ - FakeGitRepository::open(Path::new("/.git"), 0, Default::default()), - FakeGitRepository::open(Path::new("/a/.git"), 0, Default::default()), - FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), - ]; - - let new_repos: Vec> = vec![ - FakeGitRepository::open(Path::new("/a/.git"), 1, Default::default()), - FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), - FakeGitRepository::open(Path::new("/a/c/.git"), 0, Default::default()), - ]; + // let prev_repos: Vec> = vec![ + // FakeGitRepository::open(Path::new("/.git"), 0, Default::default()), + // FakeGitRepository::open(Path::new("/a/.git"), 0, Default::default()), + // FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), + // ]; + + // let new_repos: Vec> = vec![ + // FakeGitRepository::open(Path::new("/a/.git"), 1, Default::default()), + // FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), + // FakeGitRepository::open(Path::new("/a/c/.git"), 0, Default::default()), + // ]; let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); From af0974264cd61f21ae46f8b0cf8eee3025314d5b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 30 Sep 2022 17:33:34 -0700 Subject: [PATCH 53/73] Refactored git repository code to seperate out repository entry tracking data and git2 mocking code. Co-authored-by: Max Co-authored-by: Julia --- Cargo.lock | 1 + crates/collab/src/integration_tests.rs | 12 +- crates/git/Cargo.toml | 1 + crates/git/src/repository.rs | 73 ++-------- crates/project/src/fs.rs | 29 ++-- crates/project/src/project.rs | 11 +- crates/project/src/worktree.rs | 193 +++++++++++++------------ 7 files changed, 144 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75dd5530c9175814d9b101372d76e29818a16a3a..fa8f8acbdc04d35462801cf208af7a3cf9918f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2235,6 +2235,7 @@ dependencies = [ "async-trait", "clock", "collections", + "futures", "git2", "lazy_static", "log", diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d5a4c56b7d95eae1fbabcc67d6d9326351140e2d..168231a6b4e42838e34e2a2d9545d9f0e2ca53fe 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -966,7 +966,8 @@ async fn test_git_head_text( .insert_tree( "/dir", json!({ - ".git": {}, + ".git": { + }, "a.txt": " one two @@ -983,9 +984,8 @@ async fn test_git_head_text( .unindent(); let new_head_text = " - 1 + one two - three " .unindent(); @@ -1041,7 +1041,6 @@ async fn test_git_head_text( ); }); - // TODO: Create a dummy file event client_a .fs .as_fake() @@ -1051,19 +1050,18 @@ async fn test_git_head_text( ) .await; - // TODO: Flush this file event - // Wait for buffer_a to receive it executor.run_until_parked(); // Smoke test new diffing buffer_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); + git::diff::assert_hunks( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, &head_text, - &[(0..1, "1", "one\n")], + &[(2..3, "", "three\n")], ); }); diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 744fdc8b9985912ce61dbb60d94d6c88866f6707..b8f3aac0b9b2970a4c13ebb72ecd150e5535c6ca 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -19,6 +19,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } smol = "1.2" parking_lot = "0.11.1" async-trait = "0.1" +futures = "0.3" [dev-dependencies] unindent = "0.1.7" diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index ba8faa4b2b7ccb0ef68ea223b6e1b34aaac9f8a6..a49a1e0b600484d094f5c19b86adfd6cf1e3631e 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,39 +1,20 @@ use anyhow::Result; use collections::HashMap; -use git2::Repository as LibGitRepository; use parking_lot::Mutex; -use util::ResultExt; -use std::{path::{Path, PathBuf}, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +pub use git2::Repository as LibGitRepository; #[async_trait::async_trait] pub trait GitRepository: Send { - // fn manages(&self, path: &Path) -> bool; - // fn reopen_git_repo(&mut self) -> bool; - // fn git_repo(&self) -> Arc>; - // fn boxed_clone(&self) -> Box; - fn load_head_text(&self, relative_file_path: &Path) -> Option; - - fn open_real(dotgit_path: &Path) -> Option>> - where Self: Sized - { - LibGitRepository::open(&dotgit_path) - .log_err() - .and_then::>, _>(|libgit_repository| { - Some(Arc::new(Mutex::new(libgit_repository))) - }) - } } #[async_trait::async_trait] impl GitRepository for LibGitRepository { - // fn manages(&self, path: &Path) -> bool { - // path.canonicalize() - // .map(|path| path.starts_with(&self.content_path)) - // .unwrap_or(false) - // } - - fn load_head_text(&self, relative_file_path: &Path) -> Option { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { const STAGE_NORMAL: i32 = 0; @@ -56,10 +37,8 @@ impl GitRepository for LibGitRepository { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct FakeGitRepository { - content_path: Arc, - git_dir_path: Arc, state: Arc>, } @@ -69,47 +48,15 @@ pub struct FakeGitRepositoryState { } impl FakeGitRepository { - pub fn open(dotgit_path: &Path, state: Arc>) -> Box { - Box::new(FakeGitRepository { - content_path: dotgit_path.parent().unwrap().into(), - git_dir_path: dotgit_path.into(), - state, - }) + pub fn open(state: Arc>) -> Arc> { + Arc::new(Mutex::new(FakeGitRepository { state })) } } #[async_trait::async_trait] impl GitRepository for FakeGitRepository { - fn manages(&self, path: &Path) -> bool { - path.starts_with(self.content_path()) - } - - // fn in_dot_git(&self, path: &Path) -> bool { - // path.starts_with(self.git_dir_path()) - // } - - fn content_path(&self) -> &Path { - &self.content_path - } - - fn git_dir_path(&self) -> &Path { - &self.git_dir_path - } - - async fn load_head_text(&self, path: &Path) -> Option { + fn load_head_text(&self, path: &Path) -> Option { let state = self.state.lock(); state.index_contents.get(path).cloned() } - - fn reopen_git_repo(&mut self) -> bool { - true - } - - fn git_repo(&self) -> Arc> { - unimplemented!() - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 1280fcb8bcfc6d16f0fa311f0c6122843472f0a4..d0e549c0b5c35ad6abc702cc0e1bbedca0f093c5 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{FakeGitRepositoryState, GitRepository, Git2Repo}; +use git::repository::{FakeGitRepositoryState, GitRepository, LibGitRepository}; use language::LineEnding; +use parking_lot::Mutex as SyncMutex; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ io, @@ -11,6 +12,7 @@ use std::{ pin::Pin, time::{Duration, SystemTime}, }; +use util::ResultExt; use text::Rope; @@ -44,7 +46,7 @@ pub trait Fs: Send + Sync { path: &Path, latency: Duration, ) -> Pin>>>; - fn open_repo(&self, abs_dot_git: &Path) -> Option>; + fn open_repo(&self, abs_dot_git: &Path) -> Option>>; fn is_fake(&self) -> bool; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs; @@ -238,8 +240,12 @@ impl Fs for RealFs { }))) } - fn open_repo(&self, abs_dot_git: &Path) -> Option> { - Git2Repo::open(&abs_dot_git) + fn open_repo(&self, dotgit_path: &Path) -> Option>> { + LibGitRepository::open(&dotgit_path) + .log_err() + .and_then::>, _>(|libgit_repository| { + Some(Arc::new(SyncMutex::new(libgit_repository))) + }) } fn is_fake(&self) -> bool { @@ -277,7 +283,7 @@ enum FakeFsEntry { inode: u64, mtime: SystemTime, entries: BTreeMap>>, - git_repo_state: Option>>, + git_repo_state: Option>>, }, Symlink { target: PathBuf, @@ -488,7 +494,7 @@ impl FakeFs { head_state: &[(&Path, String)], ) { let content_path = dot_git.parent().unwrap(); - let state = self.state.lock().await; + let mut state = self.state.lock().await; let entry = state.read_path(dot_git).await.unwrap(); let mut entry = entry.lock().await; @@ -502,6 +508,8 @@ impl FakeFs { .iter() .map(|(path, content)| (content_path.join(path), content.clone())), ); + + state.emit_event([dot_git]); } else { panic!("not a directory"); } @@ -881,7 +889,7 @@ impl Fs for FakeFs { })) } - fn open_repo(&self, abs_dot_git: &Path) -> Option> { + fn open_repo(&self, abs_dot_git: &Path) -> Option>> { let executor = self.executor.upgrade().unwrap(); executor.block(async move { let state = self.state.lock().await; @@ -890,13 +898,10 @@ impl Fs for FakeFs { if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { let state = git_repo_state .get_or_insert_with(|| { - Arc::new(parking_lot::Mutex::new(FakeGitRepositoryState::default())) + Arc::new(SyncMutex::new(FakeGitRepositoryState::default())) }) .clone(); - Some(git::repository::FakeGitRepository::open( - abs_dot_git.into(), - state, - )) + Some(git::repository::FakeGitRepository::open(state)) } else { None } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1e8567d4d45d60f4a86e722b228e666dbf8cf133..f1aa98c4e0fa8d3b9f2a86b51ecf8ce1fd4b07b9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,7 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; -use git::repository::GitRepository; + use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, @@ -4648,7 +4648,7 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, - repos: &[Box], + repos: &[GitRepositoryEntry], cx: &mut ModelContext, ) { //TODO: Produce protos @@ -4663,12 +4663,15 @@ impl Project { let abs_path = file.abs_path(cx); let repo = match repos.iter().find(|repo| repo.manages(&abs_path)) { - Some(repo) => repo.boxed_clone(), + Some(repo) => repo.clone(), None => return, }; cx.spawn(|_, mut cx| async move { - let head_text = repo.load_head_text(&path).await; + let head_text = cx + .background() + .spawn(async move { repo.repo.lock().load_head_text(&path) }) + .await; buffer.update(&mut cx, |buffer, cx| { buffer.update_head_text(head_text, cx); }); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 4f14ab6ad121417b2eb3d40e2f17cb58f69048a4..560f23d147085dd52d878a9d8a967309189e17b4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -98,16 +98,15 @@ pub struct Snapshot { } #[derive(Clone)] -struct GitRepositoryEntry { - repo: Arc>, - - // repo: Box, - scan_id: usize, +pub struct GitRepositoryEntry { + pub(crate) repo: Arc>, + + pub(crate) scan_id: usize, // Path to folder containing the .git file or directory - content_path: Arc, + pub(crate) content_path: Arc, // Path to the actual .git folder. // Note: if .git is a file, this points to the folder indicated by the .git file - git_dir_path: Arc, + pub(crate) git_dir_path: Arc, } impl std::fmt::Debug for GitRepositoryEntry { @@ -141,11 +140,7 @@ impl Clone for LocalSnapshot { Self { abs_path: self.abs_path.clone(), ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(), - git_repositories: self - .git_repositories - .iter() - .cloned() - .collect(), + git_repositories: self.git_repositories.iter().cloned().collect(), removed_entry_ids: self.removed_entry_ids.clone(), next_entry_id: self.next_entry_id.clone(), snapshot: self.snapshot.clone(), @@ -186,7 +181,7 @@ struct ShareState { pub enum Event { UpdatedEntries, - UpdatedGitRepositories(Vec>), + UpdatedGitRepositories(Vec), } impl Entity for Worktree { @@ -610,27 +605,26 @@ impl LocalWorktree { } fn changed_repos( - old_repos: &[Box], - new_repos: &[Box], - ) -> Vec> { + old_repos: &[GitRepositoryEntry], + new_repos: &[GitRepositoryEntry], + ) -> Vec { fn diff<'a>( - a: &'a [Box], - b: &'a [Box], - updated: &mut HashMap<&'a Path, Box>, + a: &'a [GitRepositoryEntry], + b: &'a [GitRepositoryEntry], + updated: &mut HashMap<&'a Path, GitRepositoryEntry>, ) { for a_repo in a { let matched = b.iter().find(|b_repo| { - a_repo.git_dir_path() == b_repo.git_dir_path() - && a_repo.scan_id() == b_repo.scan_id() + a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id }); if matched.is_none() { - updated.insert(a_repo.git_dir_path(), a_repo.boxed_clone()); + updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone()); } } } - let mut updated = HashMap::<&Path, Box>::default(); + let mut updated = HashMap::<&Path, GitRepositoryEntry>::default(); diff(old_repos, new_repos, &mut updated); diff(new_repos, old_repos, &mut updated); @@ -690,7 +684,12 @@ impl LocalWorktree { settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked ) { let results = if let Some(repo) = snapshot.repo_for(&abs_path) { - repo.load_head_text(&path).await + cx.background() + .spawn({ + let path = path.clone(); + async move { repo.repo.lock().load_head_text(&path) } + }) + .await } else { None }; @@ -1390,25 +1389,19 @@ impl LocalSnapshot { } // Gives the most specific git repository for a given path - pub(crate) fn repo_for(&self, path: &Path) -> Option> { + pub(crate) fn repo_for(&self, path: &Path) -> Option { self.git_repositories .iter() .rev() //git_repository is ordered lexicographically - .find(|repo| repo.manages(&self.abs_path.join(path))) - .map(|repo| repo.boxed_clone()) + .find(|repo| repo.manages(path)) + .cloned() } - pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut Box> { + pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepositoryEntry> { + // Git repositories cannot be nested, so we don't need to reverse the order self.git_repositories .iter_mut() - .rev() //git_repository is ordered lexicographically - .find(|repo| repo.in_dot_git(&self.abs_path.join(path))) - } - - pub(crate) fn _tracks_filepath(&self, repo: &dyn GitRepository, file_path: &Path) -> bool { - // Depends on git_repository_for_file_path returning the most specific git repository for a given path - self.repo_for(&self.abs_path.join(file_path)) - .map_or(false, |r| r.git_dir_path() == repo.git_dir_path()) + .find(|repo| repo.in_dot_git(path)) } #[cfg(test)] @@ -1575,12 +1568,21 @@ impl LocalSnapshot { if parent_path.file_name() == Some(&DOT_GIT) { let abs_path = self.abs_path.join(&parent_path); + let content_path: Arc = parent_path.parent().unwrap().into(); if let Err(ix) = self .git_repositories - .binary_search_by_key(&abs_path.as_path(), |repo| repo.git_dir_path()) + .binary_search_by_key(&&content_path, |repo| &repo.content_path) { - if let Some(repository) = fs.open_repo(abs_path.as_path()) { - self.git_repositories.insert(ix, repository); + if let Some(repo) = fs.open_repo(abs_path.as_path()) { + self.git_repositories.insert( + ix, + GitRepositoryEntry { + repo, + scan_id: 0, + content_path, + git_dir_path: parent_path, + }, + ); } } } @@ -1673,9 +1675,9 @@ impl LocalSnapshot { let parent_path = path.parent().unwrap(); if let Ok(ix) = self .git_repositories - .binary_search_by_key(&parent_path, |repo| repo.content_path().as_ref()) + .binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref()) { - self.git_repositories[ix].set_scan_id(self.snapshot.scan_id); + self.git_repositories[ix].scan_id = self.snapshot.scan_id; } } } @@ -1716,6 +1718,25 @@ impl LocalSnapshot { ignore_stack } + + pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] { + &self.git_repositories + } +} +// Worktree root +// | +// git_dir_path: c/d/.git +//in_dot_git Query: c/d/.git/HEAD +// Manages Query: c/d/e/f/a.txt + +impl GitRepositoryEntry { + pub(crate) fn manages(&self, path: &Path) -> bool { + path.starts_with(self.content_path.as_ref()) + } + + pub(crate) fn in_dot_git(&self, path: &Path) -> bool { + path.starts_with(self.git_dir_path.as_ref()) + } } async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { @@ -2509,8 +2530,8 @@ impl BackgroundScanner { snapshot.insert_entry(fs_entry, self.fs.as_ref()); let scan_id = snapshot.scan_id; - if let Some(repo) = snapshot.in_dot_git(&abs_path) { - repo.set_scan_id(scan_id); + if let Some(repo) = snapshot.in_dot_git(&path) { + repo.scan_id = scan_id; } let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); @@ -2625,19 +2646,21 @@ impl BackgroundScanner { .await; } + // TODO: Clarify what is going on here because re-loading every git repository + // on every file system event seems wrong async fn update_git_repositories(&self) { let mut snapshot = self.snapshot.lock(); let new_repos = snapshot .git_repositories .iter() - .map(|repo| repo.boxed_clone()) - .filter_map(|mut repo| { - if repo.reopen_git_repo() { - Some(repo) - } else { - None - } + .cloned() + .filter_map(|mut repo_entry| { + let repo = self + .fs + .open_repo(&snapshot.abs_path.join(&repo_entry.git_dir_path))?; + repo_entry.repo = repo; + Some(repo_entry) }) .collect(); @@ -3262,34 +3285,17 @@ mod tests { assert!(tree.repo_for("c.txt".as_ref()).is_none()); let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - - assert_eq!( - repo.content_path(), - root.path().join("dir1").canonicalize().unwrap() - ); - assert_eq!( - repo.git_dir_path(), - root.path().join("dir1/.git").canonicalize().unwrap() - ); + assert_eq!(repo.content_path.as_ref(), Path::new("dir1")); + assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git")); let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); - - assert_eq!( - repo.content_path(), - root.path().join("dir1/deps/dep1").canonicalize().unwrap() - ); - assert_eq!( - repo.git_dir_path(), - root.path() - .join("dir1/deps/dep1/.git") - .canonicalize() - .unwrap() - ); + assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1")); + assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),); }); let original_scan_id = tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id() + tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id }); std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); @@ -3297,7 +3303,7 @@ mod tests { tree.read_with(cx, |tree, _cx| { let tree = tree.as_local().unwrap(); - let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id(); + let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id; assert_ne!( original_scan_id, new_scan_id, "original {original_scan_id}, new {new_scan_id}" @@ -3316,44 +3322,51 @@ mod tests { #[test] fn test_changed_repos() { - // let prev_repos: Vec> = vec![ - // FakeGitRepository::open(Path::new("/.git"), 0, Default::default()), - // FakeGitRepository::open(Path::new("/a/.git"), 0, Default::default()), - // FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), - // ]; - - // let new_repos: Vec> = vec![ - // FakeGitRepository::open(Path::new("/a/.git"), 1, Default::default()), - // FakeGitRepository::open(Path::new("/a/b/.git"), 0, Default::default()), - // FakeGitRepository::open(Path::new("/a/c/.git"), 0, Default::default()), - // ]; + fn fake_entry(git_dir_path: impl AsRef, scan_id: usize) -> GitRepositoryEntry { + GitRepositoryEntry { + repo: Arc::new(Mutex::new(FakeGitRepository::default())), + scan_id, + content_path: git_dir_path.as_ref().parent().unwrap().into(), + git_dir_path: git_dir_path.as_ref().into(), + } + } - let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); + let prev_repos: Vec = vec![ + fake_entry("/.git", 0), + fake_entry("/a/.git", 0), + fake_entry("/a/b/.git", 0), + ]; - dbg!(&res); + let new_repos: Vec = vec![ + fake_entry("/a/.git", 1), + fake_entry("/a/b/.git", 0), + fake_entry("/a/c/.git", 0), + ]; + + let res = LocalWorktree::changed_repos(&prev_repos, &new_repos); // Deletion retained assert!(res .iter() - .find(|repo| repo.git_dir_path() == Path::new("/.git") && repo.scan_id() == 0) + .find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0) .is_some()); // Update retained assert!(res .iter() - .find(|repo| repo.git_dir_path() == Path::new("/a/.git") && repo.scan_id() == 1) + .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1) .is_some()); // Addition retained assert!(res .iter() - .find(|repo| repo.git_dir_path() == Path::new("/a/c/.git") && repo.scan_id() == 0) + .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0) .is_some()); // Nochange, not retained assert!(res .iter() - .find(|repo| repo.git_dir_path() == Path::new("/a/b/.git") && repo.scan_id() == 0) + .find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0) .is_none()); } From a1299d9b68b2b6bf3ec01b311daabf2f54d6b1e5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 30 Sep 2022 17:34:14 -0700 Subject: [PATCH 54/73] Fixed 1 test --- crates/collab/src/integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 168231a6b4e42838e34e2a2d9545d9f0e2ca53fe..eb3fbc3dc89e5d704a056730d3229b19091b3a31 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1075,7 +1075,7 @@ async fn test_git_head_text( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, &head_text, - &[(0..1, "1", "one\n")], + &[(2..3, "", "three\n")], ); }); } From 8c24c858c9efee31e0acb55ef61ceb7fe4f0cd43 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 30 Sep 2022 17:36:22 -0700 Subject: [PATCH 55/73] Touched up comments --- crates/project/src/worktree.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 560f23d147085dd52d878a9d8a967309189e17b4..40efeee1d1586f40ee18bd85dda50029596c74fe 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -119,12 +119,6 @@ impl std::fmt::Debug for GitRepositoryEntry { } } -// impl Clone for GitRepositoryEntry { -// fn clone(&self) -> Self { -// GitRepositoryEntry { repo: self.repo.boxed_clone(), scan_id: self.scan_id } -// } -// } - pub struct LocalSnapshot { abs_path: Arc, ignores_by_parent_abs_path: HashMap, (Arc, usize)>, @@ -1723,17 +1717,14 @@ impl LocalSnapshot { &self.git_repositories } } -// Worktree root -// | -// git_dir_path: c/d/.git -//in_dot_git Query: c/d/.git/HEAD -// Manages Query: c/d/e/f/a.txt impl GitRepositoryEntry { + // Note that these paths should be relative to the worktree root. pub(crate) fn manages(&self, path: &Path) -> bool { path.starts_with(self.content_path.as_ref()) } + // Note that theis path should be relative to the worktree root. pub(crate) fn in_dot_git(&self, path: &Path) -> bool { path.starts_with(self.git_dir_path.as_ref()) } From 512f817e2f50a5917f954971c96ee763ae16b33d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 1 Oct 2022 18:18:35 -0700 Subject: [PATCH 56/73] Added proto messages for updating the head text --- crates/collab/src/integration_tests.rs | 3 -- crates/collab/src/rpc.rs | 18 +++++++++- crates/project/src/project.rs | 46 ++++++++++++++++++++++++-- crates/rpc/proto/zed.proto | 7 ++++ crates/rpc/src/proto.rs | 2 ++ 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index eb3fbc3dc89e5d704a056730d3229b19091b3a31..422c9fd0bb63b5117b47aa36809180a66fcb16c9 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1065,9 +1065,6 @@ async fn test_git_head_text( ); }); - //TODO: WAIT FOR REMOTE UPDATES TO FINISH on B - executor.run_until_parked(); - // Smoke test B buffer_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5f27352c5a08a83636e2d8f2fcfbd5c77099a226..318555b7edd1450e9342434fab06f007d91df559 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -205,7 +205,8 @@ impl Server { .add_request_handler(Server::follow) .add_message_handler(Server::unfollow) .add_message_handler(Server::update_followers) - .add_request_handler(Server::get_channel_messages); + .add_request_handler(Server::get_channel_messages) + .add_message_handler(Server::update_head_text); Arc::new(server) } @@ -1727,6 +1728,21 @@ impl Server { Ok(()) } + async fn update_head_text( + self: Arc, + request: TypedEnvelope, + ) -> Result<()> { + let receiver_ids = self.store().await.project_connection_ids( + ProjectId::from_proto(request.payload.project_id), + request.sender_id, + )?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }); + Ok(()) + } + pub(crate) async fn store(&self) -> StoreGuard<'_> { #[cfg(test)] tokio::task::yield_now().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f1aa98c4e0fa8d3b9f2a86b51ecf8ce1fd4b07b9..1064d05fe9a6610617baa21226f2f30d552fce17 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,7 +8,10 @@ pub mod worktree; mod project_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; +use client::{ + proto::{self}, + Client, PeerId, TypedEnvelope, User, UserStore, +}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; @@ -421,6 +424,7 @@ impl Project { client.add_model_request_handler(Self::handle_open_buffer_by_id); client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_save_buffer); + client.add_model_message_handler(Self::handle_update_head_text); } pub fn local( @@ -4667,14 +4671,29 @@ impl Project { None => return, }; + let shared_remote_id = self.shared_remote_id(); + let client = self.client.clone(); + cx.spawn(|_, mut cx| async move { let head_text = cx .background() .spawn(async move { repo.repo.lock().load_head_text(&path) }) .await; - buffer.update(&mut cx, |buffer, cx| { - buffer.update_head_text(head_text, cx); + + let buffer_id = buffer.update(&mut cx, |buffer, cx| { + buffer.update_head_text(head_text.clone(), cx); + buffer.remote_id() }); + + if let Some(project_id) = shared_remote_id { + client + .send(proto::UpdateHeadText { + project_id, + buffer_id: buffer_id as u64, + head_text, + }) + .log_err(); + } }) .detach(); } @@ -5253,6 +5272,27 @@ impl Project { }) } + async fn handle_update_head_text( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let buffer_id = envelope.payload.buffer_id; + let head_text = envelope.payload.head_text; + let buffer = this + .opened_buffers + .get_mut(&buffer_id) + .and_then(|b| b.upgrade(cx)) + .ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?; + + buffer.update(cx, |buffer, cx| buffer.update_head_text(head_text, cx)); + + Ok(()) + }) + } + async fn handle_update_buffer_file( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 818f2cb7e1c009c2b2d5df50a2bb83dc9462303a..d6604383da304db418dc740fa80955ecf898ec2c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -108,6 +108,7 @@ message Envelope { FollowResponse follow_response = 93; UpdateFollowers update_followers = 94; Unfollow unfollow = 95; + UpdateHeadText update_head_text = 96; } } @@ -992,3 +993,9 @@ message WorktreeMetadata { string root_name = 2; bool visible = 3; } + +message UpdateHeadText { + uint64 project_id = 1; + uint64 buffer_id = 2; + optional string head_text = 3; +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2ba3fa18bacdd7db5972a798ac63dfd8912a0eea..e91a9fd558cc5baf260ebfc2e5956bb151538113 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -167,6 +167,7 @@ messages!( (UpdateProject, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeExtensions, Background), + (UpdateHeadText, Background), ); request_messages!( @@ -263,6 +264,7 @@ entity_messages!( UpdateProject, UpdateWorktree, UpdateWorktreeExtensions, + UpdateHeadText ); entity_messages!(channel_id, ChannelMessageSent); From 7f84abaf13c1e0720de5929bf242b3a51fb525b2 Mon Sep 17 00:00:00 2001 From: Julia Date: Sun, 2 Oct 2022 14:11:35 -0400 Subject: [PATCH 57/73] Increment protocol version again for previous commit --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 2c28462ee3fd7fa67d322fb262c865ae0394af53..640271d4a2f4e496dd10b87ce460a82946c8aabd 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 33; +pub const PROTOCOL_VERSION: u32 = 34; From 5769cdc3543f953ed38573d566355e7107e8494e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 2 Oct 2022 17:56:09 -0700 Subject: [PATCH 58/73] made git diff rendering respect line wrap --- crates/editor/src/element.rs | 207 +++++++++++++++++++++++---------- crates/git/src/diff.rs | 2 +- crates/project/src/fs.rs | 10 +- crates/theme/src/theme.rs | 17 ++- styles/src/styleTree/editor.ts | 14 ++- 5 files changed, 170 insertions(+), 80 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57ee919288f8fb56fadd5a52bc71ae24a28be301..5d83051567b097092e9cd45804eba50c69fddce6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -46,6 +46,7 @@ use std::{ ops::Range, sync::Arc, }; +use theme::DiffStyle; struct SelectionLayout { head: DisplayPoint, @@ -525,98 +526,156 @@ impl EditorElement { layout: &mut LayoutState, cx: &mut PaintContext, ) { - let line_height = layout.position_map.line_height; - let scroll_position = layout.position_map.snapshot.scroll_position(); - let scroll_top = scroll_position.y() * line_height; - for (ix, line) in layout.line_number_layouts.iter().enumerate() { - if let Some(line) = line { - let line_origin = bounds.origin() - + vec2f( - bounds.width() - line.width() - layout.gutter_padding, - ix as f32 * layout.position_map.line_height - - (scroll_top % layout.position_map.line_height), - ); - line.paint( - line_origin, - visible_bounds, - layout.position_map.line_height, - cx, - ); - } + struct GutterLayout { + line_height: f32, + // scroll_position: Vector2F, + scroll_top: f32, + bounds: RectF, } - let ( - inserted_color, - modified_color, - deleted_color, - width_multiplier, - corner_radius, - removed_width_mult, - ) = { - let editor = &cx.global::().theme.editor; - ( - editor.diff_background_inserted, - editor.diff_background_modified, - editor.diff_background_deleted, - editor.diff_indicator_width_multiplier, - editor.diff_indicator_corner_radius, - editor.removed_diff_width_multiplier, - ) - }; + struct DiffLayout<'a> { + buffer_line: usize, + last_diff: Option<(&'a DiffHunk, usize)>, + } - for hunk in &layout.diff_hunks { - let color = match hunk.status() { - DiffHunkStatus::Added => inserted_color, - DiffHunkStatus::Modified => modified_color, + fn diff_quad( + status: DiffHunkStatus, + layout_range: Range, + gutter_layout: &GutterLayout, + diff_style: &DiffStyle, + ) -> Quad { + let color = match status { + DiffHunkStatus::Added => diff_style.inserted, + DiffHunkStatus::Modified => diff_style.modified, //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { - let row = hunk.buffer_range.start; + let row = layout_range.start; - let offset = line_height / 2.; - let start_y = row as f32 * line_height + offset - scroll_top; - let end_y = start_y + line_height; + let offset = gutter_layout.line_height / 2.; + let start_y = + row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top; + let end_y = start_y + gutter_layout.line_height; - let width = removed_width_mult * line_height; - let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let width = diff_style.removed_width_em * gutter_layout.line_height; + let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); - cx.scene.push_quad(Quad { + return Quad { bounds: highlight_bounds, - background: Some(deleted_color), + background: Some(diff_style.deleted), border: Border::new(0., Color::transparent_black()), - corner_radius: 1. * line_height, - }); - - continue; + corner_radius: 1. * gutter_layout.line_height, + }; } }; - let start_row = hunk.buffer_range.start; - let end_row = hunk.buffer_range.end; + let start_row = layout_range.start; + let end_row = layout_range.end; - let start_y = start_row as f32 * line_height - scroll_top; - let end_y = end_row as f32 * line_height - scroll_top; + let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top; + let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top; - let width = width_multiplier * line_height; - let highlight_origin = bounds.origin() + vec2f(-width, start_y); + let width = diff_style.width_em * gutter_layout.line_height; + let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y); let highlight_size = vec2f(width * 2., end_y - start_y); let highlight_bounds = RectF::new(highlight_origin, highlight_size); - cx.scene.push_quad(Quad { + Quad { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: corner_radius * line_height, - }); + corner_radius: diff_style.corner_radius * gutter_layout.line_height, + } + } + + let gutter_layout = { + let scroll_position = layout.position_map.snapshot.scroll_position(); + let line_height = layout.position_map.line_height; + GutterLayout { + scroll_top: scroll_position.y() * line_height, + // scroll_position, + line_height, + bounds, + } + }; + + let mut diff_layout = DiffLayout { + buffer_line: 0, + last_diff: None, + }; + + let diff_style = &cx.global::().theme.editor.diff.clone(); + // dbg!("***************"); + // dbg!(&layout.diff_hunks); + // dbg!("***************"); + + // line is `None` when there's a line wrap + for (ix, line) in layout.line_number_layouts.iter().enumerate() { + // dbg!(ix); + if let Some(line) = line { + let line_origin = bounds.origin() + + vec2f( + bounds.width() - line.width() - layout.gutter_padding, + ix as f32 * gutter_layout.line_height + - (gutter_layout.scroll_top % gutter_layout.line_height), + ); + + line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx); + + //This line starts a buffer line, so let's do the diff calculation + let new_hunk = get_hunk(diff_layout.buffer_line, &layout.diff_hunks); + + // This + the unwraps are annoying, but at least it's legible + let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) { + (None, None) => (false, false), + (None, Some(_)) => (false, true), + (Some(_), None) => (true, false), + (Some((old_hunk, _)), Some(new_hunk)) if new_hunk == old_hunk => (false, false), + (Some(_), Some(_)) => (true, true), + }; + + // dbg!(diff_layout.buffer_line, is_starting); + + if is_ending { + let (last_hunk, start_line) = diff_layout.last_diff.take().unwrap(); + // dbg!("ending"); + // dbg!(start_line..ix); + cx.scene.push_quad(diff_quad( + last_hunk.status(), + start_line..ix, + &gutter_layout, + diff_style, + )); + } + + if is_starting { + let new_hunk = new_hunk.unwrap(); + + diff_layout.last_diff = Some((new_hunk, ix)); + }; + + diff_layout.buffer_line += 1; + } + } + + // If we ran out with a diff hunk still being prepped, paint it now + if let Some((last_hunk, start_line)) = diff_layout.last_diff { + let end_line = layout.line_number_layouts.len(); + cx.scene.push_quad(diff_quad( + last_hunk.status(), + start_line..end_line, + &gutter_layout, + diff_style, + )) } if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; - let mut y = *row as f32 * line_height - scroll_top; + let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top; x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; - y += (line_height - indicator.size().y()) / 2.; + y += (gutter_layout.line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } } @@ -1321,6 +1380,28 @@ impl EditorElement { } } +/// Get the hunk that contains buffer_line, starting from start_idx +/// Returns none if there is none found, and +fn get_hunk(buffer_line: usize, hunks: &[DiffHunk]) -> Option<&DiffHunk> { + for i in 0..hunks.len() { + // Safety: Index out of bounds is handled by the check above + let hunk = hunks.get(i).unwrap(); + if hunk.buffer_range.contains(&(buffer_line as u32)) { + return Some(hunk); + } else if hunk.status() == DiffHunkStatus::Removed + && buffer_line == hunk.buffer_range.start as usize + { + return Some(hunk); + } else if hunk.buffer_range.start > buffer_line as u32 { + // If we've passed the buffer_line, just stop + return None; + } + } + + // We reached the end of the array without finding a hunk, just return none. + return None; +} + impl Element for EditorElement { type LayoutState = LayoutState; type PaintState = (); diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 6c904d44d1ee4322a3c62f6ce788d44a799fbf7c..48630fc91cbd0d6663b6f506deb4b1fd23ec593d 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -6,7 +6,7 @@ use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiffHunkStatus { Added, Modified, diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index d0e549c0b5c35ad6abc702cc0e1bbedca0f093c5..2b7aca642da2ad442b66afb496b12f20add31949 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -1,10 +1,11 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{FakeGitRepositoryState, GitRepository, LibGitRepository}; +use git::repository::{GitRepository, LibGitRepository}; use language::LineEnding; use parking_lot::Mutex as SyncMutex; use smol::io::{AsyncReadExt, AsyncWriteExt}; +use std::sync::Arc; use std::{ io, os::unix::fs::MetadataExt, @@ -12,16 +13,17 @@ use std::{ pin::Pin, time::{Duration, SystemTime}, }; -use util::ResultExt; - use text::Rope; +use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] use futures::lock::Mutex; #[cfg(any(test, feature = "test-support"))] -use std::sync::{Arc, Weak}; +use git::repository::FakeGitRepositoryState; +#[cfg(any(test, feature = "test-support"))] +use std::sync::Weak; #[async_trait::async_trait] pub trait Fs: Send + Sync { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0d0c94ea8db1f9143efb1abd3e0d68b1019192e2..d8c829648156654d51cad54ea5b5d000f0cb3f08 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -488,12 +488,7 @@ pub struct Editor { pub rename_fade: f32, pub document_highlight_read_background: Color, pub document_highlight_write_background: Color, - pub diff_background_deleted: Color, - pub diff_background_inserted: Color, - pub diff_background_modified: Color, - pub removed_diff_width_multiplier: f32, - pub diff_indicator_width_multiplier: f32, - pub diff_indicator_corner_radius: f32, + pub diff: DiffStyle, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, @@ -577,6 +572,16 @@ pub struct CodeActions { pub vertical_scale: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct DiffStyle { + pub inserted: Color, + pub modified: Color, + pub deleted: Color, + pub removed_width_em: f32, + pub width_em: f32, + pub corner_radius: f32, +} + #[derive(Debug, Default, Clone, Copy)] pub struct Interactive { pub default: T, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 6e52c620ee474ddc1f328b42323205a7478b4928..04a5bafbd57276f4dbacf6b44dfa6a29aec68378 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -60,12 +60,14 @@ export default function editor(theme: Theme) { indicator: iconColor(theme, "secondary"), verticalScale: 0.618 }, - diffBackgroundDeleted: theme.iconColor.error, - diffBackgroundInserted: theme.iconColor.ok, - diffBackgroundModified: theme.iconColor.warning, - removedDiffWidthMultiplier: 0.275, - diffIndicatorWidthMultiplier: 0.16, - diffIndicatorCornerRadius: 0.05, + diff: { + deleted: theme.iconColor.error, + inserted: theme.iconColor.ok, + modified: theme.iconColor.warning, + removedWidthEm: 0.275, + widthEm: 0.16, + cornerRadius: 0.05, + }, documentHighlightReadBackground: theme.editor.highlight.occurrence, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence, errorColor: theme.textColor.error, From 52dbf2f9b8246bb5b1258ab990939acb74efc527 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 2 Oct 2022 18:01:37 -0700 Subject: [PATCH 59/73] add proto stuff --- crates/client/src/client.rs | 4 +- crates/client/src/telemetry.rs | 10 +- crates/client/src/user.rs | 10 +- .../20220913211150_create_signups.down.sql | 6 - ....sql => 20220913211150_create_signups.sql} | 0 .../20220929182110_add_metrics_id.sql | 2 + crates/collab/src/api.rs | 82 ++-- crates/collab/src/db.rs | 56 ++- crates/collab/src/db_tests.rs | 349 +++++++++--------- crates/collab/src/integration_tests.rs | 7 +- crates/collab/src/rpc.rs | 17 + crates/rpc/proto/zed.proto | 10 +- crates/rpc/src/proto.rs | 3 + 13 files changed, 317 insertions(+), 239 deletions(-) delete mode 100644 crates/collab/migrations/20220913211150_create_signups.down.sql rename crates/collab/migrations/{20220913211150_create_signups.up.sql => 20220913211150_create_signups.sql} (100%) create mode 100644 crates/collab/migrations/20220929182110_add_metrics_id.sql diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b75be6230875b42608cdfa3a4d52028674d44ed9..9ec24abae58a40d71646fd5f6228d3c47ba97864 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -320,11 +320,9 @@ impl Client { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; - let user_id = state.credentials.as_ref().map(|c| c.user_id); match status { Status::Connected { .. } => { - self.telemetry.set_user_id(user_id); state._reconnect_task = None; } Status::ConnectionLost => { @@ -353,7 +351,7 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { - self.telemetry.set_user_id(user_id); + self.telemetry.set_metrics_id(None); state._reconnect_task.take(); } _ => {} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8b7be5ba8089503be1763313695e4f88baee5504..c9b5665e9ed7a786c354caa3338d4b05754f8002 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -29,7 +29,7 @@ pub struct Telemetry { #[derive(Default)] struct TelemetryState { - user_id: Option>, + metrics_id: Option>, device_id: Option>, app_version: Option>, os_version: Option>, @@ -115,7 +115,7 @@ impl Telemetry { flush_task: Default::default(), next_event_id: 0, log_file: None, - user_id: None, + metrics_id: None, }), }); @@ -176,8 +176,8 @@ impl Telemetry { .detach(); } - pub fn set_user_id(&self, user_id: Option) { - self.state.lock().user_id = user_id.map(|id| id.to_string().into()); + pub fn set_metrics_id(&self, metrics_id: Option) { + self.state.lock().metrics_id = metrics_id.map(|s| s.into()); } pub fn report_event(self: &Arc, kind: &str, properties: Value) { @@ -199,7 +199,7 @@ impl Telemetry { None }, user_properties: None, - user_id: state.user_id.clone(), + user_id: state.metrics_id.clone(), device_id: state.device_id.clone(), os_name: state.os_name, os_version: state.os_version.clone(), diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 149d22e77aec8fdb0803764eb7f9c863ed018797..b31cda94b37f1310b14772f0027706d59368055b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -142,10 +142,14 @@ impl UserStore { match status { Status::Connected { .. } => { if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { - let user = this + let fetch_user = this .update(&mut cx, |this, cx| this.fetch_user(user_id, cx)) - .log_err() - .await; + .log_err(); + let fetch_metrics_id = + client.request(proto::GetPrivateUserInfo {}).log_err(); + let (user, info) = futures::join!(fetch_user, fetch_metrics_id); + client.telemetry.set_metrics_id(info.map(|i| i.metrics_id)); + client.telemetry.report_event("sign in", Default::default()); current_user_tx.send(user).await.ok(); } } diff --git a/crates/collab/migrations/20220913211150_create_signups.down.sql b/crates/collab/migrations/20220913211150_create_signups.down.sql deleted file mode 100644 index 5504bbb8dc89ace57009f9ff24e8c65394610e04..0000000000000000000000000000000000000000 --- a/crates/collab/migrations/20220913211150_create_signups.down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE signups; - -ALTER TABLE users - DROP COLUMN github_user_id; - -DROP INDEX index_users_on_email_address; diff --git a/crates/collab/migrations/20220913211150_create_signups.up.sql b/crates/collab/migrations/20220913211150_create_signups.sql similarity index 100% rename from crates/collab/migrations/20220913211150_create_signups.up.sql rename to crates/collab/migrations/20220913211150_create_signups.sql diff --git a/crates/collab/migrations/20220929182110_add_metrics_id.sql b/crates/collab/migrations/20220929182110_add_metrics_id.sql new file mode 100644 index 0000000000000000000000000000000000000000..665d6323bf13b5553859ae6763392770dc33bebb --- /dev/null +++ b/crates/collab/migrations/20220929182110_add_metrics_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE "users" + ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid(); diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 0a9d8106ce2d8530c60911eda83b59066774ae92..08dfa91ba98df3b1c54e8690dbd31bd169da936c 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -24,6 +24,7 @@ use tracing::instrument; pub fn routes(rpc_server: &Arc, state: Arc) -> Router { Router::new() + .route("/user", get(get_authenticated_user)) .route("/users", get(get_users).post(create_user)) .route("/users/:id", put(update_user).delete(destroy_user)) .route("/users/:id/access_tokens", post(create_access_token)) @@ -85,10 +86,33 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } +#[derive(Debug, Deserialize)] +struct AuthenticatedUserParams { + github_user_id: i32, + github_login: String, +} + +#[derive(Debug, Serialize)] +struct AuthenticatedUserResponse { + user: User, + metrics_id: String, +} + +async fn get_authenticated_user( + Query(params): Query, + Extension(app): Extension>, +) -> Result> { + let user = app + .db + .get_user_by_github_account(¶ms.github_login, Some(params.github_user_id)) + .await? + .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?; + let metrics_id = app.db.get_user_metrics_id(user.id).await?; + return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); +} + #[derive(Debug, Deserialize)] struct GetUsersQueryParams { - github_user_id: Option, - github_login: Option, query: Option, page: Option, limit: Option, @@ -98,14 +122,6 @@ async fn get_users( Query(params): Query, Extension(app): Extension>, ) -> Result>> { - if let Some(github_login) = ¶ms.github_login { - let user = app - .db - .get_user_by_github_account(github_login, params.github_user_id) - .await?; - return Ok(Json(Vec::from_iter(user))); - } - let limit = params.limit.unwrap_or(100); let users = if let Some(query) = params.query { app.db.fuzzy_search_users(&query, limit).await? @@ -124,6 +140,8 @@ struct CreateUserParams { email_address: String, email_confirmation_code: Option, #[serde(default)] + admin: bool, + #[serde(default)] invite_count: i32, } @@ -131,6 +149,7 @@ struct CreateUserParams { struct CreateUserResponse { user: User, signup_device_id: Option, + metrics_id: String, } async fn create_user( @@ -143,12 +162,10 @@ async fn create_user( github_user_id: params.github_user_id, invite_count: params.invite_count, }; - let user_id; - let signup_device_id; + // Creating a user via the normal signup process - if let Some(email_confirmation_code) = params.email_confirmation_code { - let result = app - .db + let result = if let Some(email_confirmation_code) = params.email_confirmation_code { + app.db .create_user_from_invite( &Invite { email_address: params.email_address, @@ -156,34 +173,37 @@ async fn create_user( }, user, ) - .await?; - user_id = result.user_id; - signup_device_id = result.signup_device_id; - if let Some(inviter_id) = result.inviting_user_id { - rpc_server - .invite_code_redeemed(inviter_id, user_id) - .await - .trace_err(); - } + .await? } // Creating a user as an admin - else { - user_id = app - .db + else if params.admin { + app.db .create_user(¶ms.email_address, false, user) - .await?; - signup_device_id = None; + .await? + } else { + Err(Error::Http( + StatusCode::UNPROCESSABLE_ENTITY, + "email confirmation code is required".into(), + ))? + }; + + if let Some(inviter_id) = result.inviting_user_id { + rpc_server + .invite_code_redeemed(inviter_id, result.user_id) + .await + .trace_err(); } let user = app .db - .get_user_by_id(user_id) + .get_user_by_id(result.user_id) .await? .ok_or_else(|| anyhow!("couldn't find the user we just created"))?; Ok(Json(CreateUserResponse { user, - signup_device_id, + metrics_id: result.metrics_id, + signup_device_id: result.signup_device_id, })) } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8b01cdf971e40ee88dd8a55dd9d88d8747b0795b..a12f6a4f89db7b6ea5998e510f3044bbfbfba1a7 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -17,10 +17,11 @@ pub trait Db: Send + Sync { email_address: &str, admin: bool, params: NewUserParams, - ) -> Result; + ) -> Result; async fn get_all_users(&self, page: u32, limit: u32) -> Result>; async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result>; async fn get_user_by_id(&self, id: UserId) -> Result>; + async fn get_user_metrics_id(&self, id: UserId) -> Result; async fn get_users_by_ids(&self, ids: Vec) -> Result>; async fn get_users_with_no_invites(&self, invited_by_another_user: bool) -> Result>; async fn get_user_by_github_account( @@ -208,21 +209,26 @@ impl Db for PostgresDb { email_address: &str, admin: bool, params: NewUserParams, - ) -> Result { + ) -> Result { let query = " INSERT INTO users (email_address, github_login, github_user_id, admin) VALUES ($1, $2, $3, $4) ON CONFLICT (github_login) DO UPDATE SET github_login = excluded.github_login - RETURNING id + RETURNING id, metrics_id::text "; - Ok(sqlx::query_scalar(query) + let (user_id, metrics_id): (UserId, String) = sqlx::query_as(query) .bind(email_address) .bind(params.github_login) .bind(params.github_user_id) .bind(admin) .fetch_one(&self.pool) - .await - .map(UserId)?) + .await?; + Ok(NewUserResult { + user_id, + metrics_id, + signup_device_id: None, + inviting_user_id: None, + }) } async fn get_all_users(&self, page: u32, limit: u32) -> Result> { @@ -256,6 +262,18 @@ impl Db for PostgresDb { Ok(users.into_iter().next()) } + async fn get_user_metrics_id(&self, id: UserId) -> Result { + let query = " + SELECT metrics_id::text + FROM users + WHERE id = $1 + "; + Ok(sqlx::query_scalar(query) + .bind(id) + .fetch_one(&self.pool) + .await?) + } + async fn get_users_by_ids(&self, ids: Vec) -> Result> { let ids = ids.into_iter().map(|id| id.0).collect::>(); let query = " @@ -493,13 +511,13 @@ impl Db for PostgresDb { ))?; } - let user_id: UserId = sqlx::query_scalar( + let (user_id, metrics_id): (UserId, String) = sqlx::query_as( " INSERT INTO users (email_address, github_login, github_user_id, admin, invite_count, invite_code) VALUES ($1, $2, $3, 'f', $4, $5) - RETURNING id + RETURNING id, metrics_id::text ", ) .bind(&invite.email_address) @@ -559,6 +577,7 @@ impl Db for PostgresDb { tx.commit().await?; Ok(NewUserResult { user_id, + metrics_id, inviting_user_id, signup_device_id, }) @@ -1722,6 +1741,7 @@ pub struct NewUserParams { #[derive(Debug)] pub struct NewUserResult { pub user_id: UserId, + pub metrics_id: String, pub inviting_user_id: Option, pub signup_device_id: Option, } @@ -1808,15 +1828,15 @@ mod test { email_address: &str, admin: bool, params: NewUserParams, - ) -> Result { + ) -> Result { self.background.simulate_random_delay().await; let mut users = self.users.lock(); - if let Some(user) = users + let user_id = if let Some(user) = users .values() .find(|user| user.github_login == params.github_login) { - Ok(user.id) + user.id } else { let id = post_inc(&mut *self.next_user_id.lock()); let user_id = UserId(id); @@ -1833,8 +1853,14 @@ mod test { connected_once: false, }, ); - Ok(user_id) - } + user_id + }; + Ok(NewUserResult { + user_id, + metrics_id: "the-metrics-id".to_string(), + inviting_user_id: None, + signup_device_id: None, + }) } async fn get_all_users(&self, _page: u32, _limit: u32) -> Result> { @@ -1850,6 +1876,10 @@ mod test { Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next()) } + async fn get_user_metrics_id(&self, _id: UserId) -> Result { + Ok("the-metrics-id".to_string()) + } + async fn get_users_by_ids(&self, ids: Vec) -> Result> { self.background.simulate_random_delay().await; let users = self.users.lock(); diff --git a/crates/collab/src/db_tests.rs b/crates/collab/src/db_tests.rs index 1e48b4b7540e8a7d110fb3a673a5878225384cd1..e063b97eb6ffed39e6cbe99e814432ba3fe5b1f4 100644 --- a/crates/collab/src/db_tests.rs +++ b/crates/collab/src/db_tests.rs @@ -12,89 +12,56 @@ async fn test_get_users_by_ids() { ] { let db = test_db.db(); - let user1 = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - "u2@example.com", - false, - NewUserParams { - github_login: "u2".into(), - github_user_id: 2, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user3 = db - .create_user( - "u3@example.com", - false, - NewUserParams { - github_login: "u3".into(), - github_user_id: 3, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user4 = db - .create_user( - "u4@example.com", - false, - NewUserParams { - github_login: "u4".into(), - github_user_id: 4, - invite_count: 0, - }, - ) - .await - .unwrap(); + let mut user_ids = Vec::new(); + for i in 1..=4 { + user_ids.push( + db.create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id, + ); + } assert_eq!( - db.get_users_by_ids(vec![user1, user2, user3, user4]) - .await - .unwrap(), + db.get_users_by_ids(user_ids.clone()).await.unwrap(), vec![ User { - id: user1, - github_login: "u1".to_string(), + id: user_ids[0], + github_login: "user1".to_string(), github_user_id: Some(1), - email_address: Some("u1@example.com".to_string()), + email_address: Some("user1@example.com".to_string()), admin: false, ..Default::default() }, User { - id: user2, - github_login: "u2".to_string(), + id: user_ids[1], + github_login: "user2".to_string(), github_user_id: Some(2), - email_address: Some("u2@example.com".to_string()), + email_address: Some("user2@example.com".to_string()), admin: false, ..Default::default() }, User { - id: user3, - github_login: "u3".to_string(), + id: user_ids[2], + github_login: "user3".to_string(), github_user_id: Some(3), - email_address: Some("u3@example.com".to_string()), + email_address: Some("user3@example.com".to_string()), admin: false, ..Default::default() }, User { - id: user4, - github_login: "u4".to_string(), + id: user_ids[3], + github_login: "user4".to_string(), github_user_id: Some(4), - email_address: Some("u4@example.com".to_string()), + email_address: Some("user4@example.com".to_string()), admin: false, ..Default::default() } @@ -121,7 +88,8 @@ async fn test_get_user_by_github_account() { }, ) .await - .unwrap(); + .unwrap() + .user_id; let user_id2 = db .create_user( "user2@example.com", @@ -133,7 +101,8 @@ async fn test_get_user_by_github_account() { }, ) .await - .unwrap(); + .unwrap() + .user_id; let user = db .get_user_by_github_account("login1", None) @@ -177,7 +146,8 @@ async fn test_worktree_extensions() { }, ) .await - .unwrap(); + .unwrap() + .user_id; let project = db.register_project(user).await.unwrap(); db.update_worktree_extensions(project, 100, Default::default()) @@ -237,43 +207,25 @@ async fn test_user_activity() { let test_db = TestDb::postgres().await; let db = test_db.db(); - let user_1 = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user_2 = db - .create_user( - "u2@example.com", - false, - NewUserParams { - github_login: "u2".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user_3 = db - .create_user( - "u3@example.com", - false, - NewUserParams { - github_login: "u3".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let project_1 = db.register_project(user_1).await.unwrap(); + let mut user_ids = Vec::new(); + for i in 0..=2 { + user_ids.push( + db.create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id, + ); + } + + let project_1 = db.register_project(user_ids[0]).await.unwrap(); db.update_worktree_extensions( project_1, 1, @@ -281,34 +233,37 @@ async fn test_user_activity() { ) .await .unwrap(); - let project_2 = db.register_project(user_2).await.unwrap(); + let project_2 = db.register_project(user_ids[1]).await.unwrap(); let t0 = OffsetDateTime::now_utc() - Duration::from_secs(60 * 60); // User 2 opens a project let t1 = t0 + Duration::from_secs(10); - db.record_user_activity(t0..t1, &[(user_2, project_2)]) + db.record_user_activity(t0..t1, &[(user_ids[1], project_2)]) .await .unwrap(); let t2 = t1 + Duration::from_secs(10); - db.record_user_activity(t1..t2, &[(user_2, project_2)]) + db.record_user_activity(t1..t2, &[(user_ids[1], project_2)]) .await .unwrap(); // User 1 joins the project let t3 = t2 + Duration::from_secs(10); - db.record_user_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)]) - .await - .unwrap(); + db.record_user_activity( + t2..t3, + &[(user_ids[1], project_2), (user_ids[0], project_2)], + ) + .await + .unwrap(); // User 1 opens another project let t4 = t3 + Duration::from_secs(10); db.record_user_activity( t3..t4, &[ - (user_2, project_2), - (user_1, project_2), - (user_1, project_1), + (user_ids[1], project_2), + (user_ids[0], project_2), + (user_ids[0], project_1), ], ) .await @@ -319,10 +274,10 @@ async fn test_user_activity() { db.record_user_activity( t4..t5, &[ - (user_2, project_2), - (user_1, project_2), - (user_1, project_1), - (user_3, project_1), + (user_ids[1], project_2), + (user_ids[0], project_2), + (user_ids[0], project_1), + (user_ids[2], project_1), ], ) .await @@ -330,13 +285,16 @@ async fn test_user_activity() { // User 2 leaves let t6 = t5 + Duration::from_secs(5); - db.record_user_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)]) - .await - .unwrap(); + db.record_user_activity( + t5..t6, + &[(user_ids[0], project_1), (user_ids[2], project_1)], + ) + .await + .unwrap(); let t7 = t6 + Duration::from_secs(60); let t8 = t7 + Duration::from_secs(10); - db.record_user_activity(t7..t8, &[(user_1, project_1)]) + db.record_user_activity(t7..t8, &[(user_ids[0], project_1)]) .await .unwrap(); @@ -344,8 +302,8 @@ async fn test_user_activity() { db.get_top_users_activity_summary(t0..t6, 10).await.unwrap(), &[ UserActivitySummary { - id: user_1, - github_login: "u1".to_string(), + id: user_ids[0], + github_login: "user0".to_string(), project_activity: vec![ ProjectActivitySummary { id: project_1, @@ -360,8 +318,8 @@ async fn test_user_activity() { ] }, UserActivitySummary { - id: user_2, - github_login: "u2".to_string(), + id: user_ids[1], + github_login: "user1".to_string(), project_activity: vec![ProjectActivitySummary { id: project_2, duration: Duration::from_secs(50), @@ -369,8 +327,8 @@ async fn test_user_activity() { }] }, UserActivitySummary { - id: user_3, - github_login: "u3".to_string(), + id: user_ids[2], + github_login: "user2".to_string(), project_activity: vec![ProjectActivitySummary { id: project_1, duration: Duration::from_secs(15), @@ -442,7 +400,9 @@ async fn test_user_activity() { ); assert_eq!( - db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(), + db.get_user_activity_timeline(t3..t6, user_ids[0]) + .await + .unwrap(), &[ UserActivityPeriod { project_id: project_1, @@ -459,7 +419,9 @@ async fn test_user_activity() { ] ); assert_eq!( - db.get_user_activity_timeline(t0..t8, user_1).await.unwrap(), + db.get_user_activity_timeline(t0..t8, user_ids[0]) + .await + .unwrap(), &[ UserActivityPeriod { project_id: project_2, @@ -501,7 +463,8 @@ async fn test_recent_channel_messages() { }, ) .await - .unwrap(); + .unwrap() + .user_id; let org = db.create_org("org", "org").await.unwrap(); let channel = db.create_org_channel(org, "channel").await.unwrap(); for i in 0..10 { @@ -545,7 +508,8 @@ async fn test_channel_message_nonces() { }, ) .await - .unwrap(); + .unwrap() + .user_id; let org = db.create_org("org", "org").await.unwrap(); let channel = db.create_org_channel(org, "channel").await.unwrap(); @@ -587,7 +551,8 @@ async fn test_create_access_tokens() { }, ) .await - .unwrap(); + .unwrap() + .user_id; db.create_access_token_hash(user, "h1", 3).await.unwrap(); db.create_access_token_hash(user, "h2", 3).await.unwrap(); @@ -678,42 +643,27 @@ async fn test_add_contacts() { ] { let db = test_db.db(); - let user_1 = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user_2 = db - .create_user( - "u2@example.com", - false, - NewUserParams { - github_login: "u2".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user_3 = db - .create_user( - "u3@example.com", - false, - NewUserParams { - github_login: "u3".into(), - github_user_id: 2, - invite_count: 0, - }, - ) - .await - .unwrap(); + let mut user_ids = Vec::new(); + for i in 0..3 { + user_ids.push( + db.create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id, + ); + } + + let user_1 = user_ids[0]; + let user_2 = user_ids[1]; + let user_3 = user_ids[2]; // User starts with no contacts assert_eq!( @@ -927,12 +877,12 @@ async fn test_add_contacts() { async fn test_invite_codes() { let postgres = TestDb::postgres().await; let db = postgres.db(); - let user1 = db + let NewUserResult { user_id: user1, .. } = db .create_user( - "u1@example.com", + "user1@example.com", false, NewUserParams { - github_login: "u1".into(), + github_login: "user1".into(), github_user_id: 0, invite_count: 0, }, @@ -954,13 +904,14 @@ async fn test_invite_codes() { // User 2 redeems the invite code and becomes a contact of user 1. let user2_invite = db - .create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id")) + .create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id")) .await .unwrap(); let NewUserResult { user_id: user2, inviting_user_id, signup_device_id, + metrics_id, } = db .create_user_from_invite( &user2_invite, @@ -976,6 +927,7 @@ async fn test_invite_codes() { assert_eq!(invite_count, 1); assert_eq!(inviting_user_id, Some(user1)); assert_eq!(signup_device_id.unwrap(), "user-2-device-id"); + assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id); assert_eq!( db.get_contacts(user1).await.unwrap(), [ @@ -1009,13 +961,14 @@ async fn test_invite_codes() { // User 3 redeems the invite code and becomes a contact of user 1. let user3_invite = db - .create_invite_from_code(&invite_code, "u3@example.com", None) + .create_invite_from_code(&invite_code, "user3@example.com", None) .await .unwrap(); let NewUserResult { user_id: user3, inviting_user_id, signup_device_id, + .. } = db .create_user_from_invite( &user3_invite, @@ -1067,7 +1020,7 @@ async fn test_invite_codes() { ); // Trying to reedem the code for the third time results in an error. - db.create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id")) + db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id")) .await .unwrap_err(); @@ -1079,7 +1032,7 @@ async fn test_invite_codes() { // User 4 can now redeem the invite code and becomes a contact of user 1. let user4_invite = db - .create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id")) + .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id")) .await .unwrap(); let user4 = db @@ -1137,7 +1090,7 @@ async fn test_invite_codes() { ); // An existing user cannot redeem invite codes. - db.create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id")) + db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id")) .await .unwrap_err(); let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); @@ -1232,6 +1185,7 @@ async fn test_signups() { user_id, inviting_user_id, signup_device_id, + .. } = db .create_user_from_invite( &Invite { @@ -1284,6 +1238,51 @@ async fn test_signups() { .unwrap_err(); } +#[tokio::test(flavor = "multi_thread")] +async fn test_metrics_id() { + let postgres = TestDb::postgres().await; + let db = postgres.db(); + + let NewUserResult { + user_id: user1, + metrics_id: metrics_id1, + .. + } = db + .create_user( + "person1@example.com", + false, + NewUserParams { + github_login: "person1".into(), + github_user_id: 101, + invite_count: 5, + }, + ) + .await + .unwrap(); + let NewUserResult { + user_id: user2, + metrics_id: metrics_id2, + .. + } = db + .create_user( + "person2@example.com", + false, + NewUserParams { + github_login: "person2".into(), + github_user_id: 102, + invite_count: 5, + }, + ) + .await + .unwrap(); + + assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1); + assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2); + assert_eq!(metrics_id1.len(), 36); + assert_eq!(metrics_id2.len(), 36); + assert_ne!(metrics_id1, metrics_id2); +} + fn build_background_executor() -> Arc { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 422c9fd0bb63b5117b47aa36809180a66fcb16c9..a13a013e7a4f624af7c0dea1fe7d11970962bfaa 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4794,7 +4794,8 @@ async fn test_random_collaboration( }, ) .await - .unwrap(); + .unwrap() + .user_id; let mut available_guests = vec![ "guest-1".to_string(), "guest-2".to_string(), @@ -4814,7 +4815,8 @@ async fn test_random_collaboration( }, ) .await - .unwrap(); + .unwrap() + .user_id; assert_eq!(*username, format!("guest-{}", guest_user_id)); server .app_state @@ -5337,6 +5339,7 @@ impl TestServer { ) .await .unwrap() + .user_id }; let client_name = name.to_string(); let mut client = cx.read(|cx| Client::new(http.clone(), cx)); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 318555b7edd1450e9342434fab06f007d91df559..15748a52cf25acec5f9c929d4bf8d5e25a08d85b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -206,7 +206,11 @@ impl Server { .add_message_handler(Server::unfollow) .add_message_handler(Server::update_followers) .add_request_handler(Server::get_channel_messages) +<<<<<<< HEAD .add_message_handler(Server::update_head_text); +======= + .add_request_handler(Server::get_private_user_info); +>>>>>>> 5d09083a (Identify users in amplitude via a separate 'metrics_id' UUID) Arc::new(server) } @@ -1742,6 +1746,19 @@ impl Server { }); Ok(()) } + async fn get_private_user_info( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let user_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id })?; + Ok(()) + } pub(crate) async fn store(&self) -> StoreGuard<'_> { #[cfg(test)] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d6604383da304db418dc740fa80955ecf898ec2c..832c5bb6bd67fb5133e120b93510862b61563315 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -108,7 +108,9 @@ message Envelope { FollowResponse follow_response = 93; UpdateFollowers update_followers = 94; Unfollow unfollow = 95; - UpdateHeadText update_head_text = 96; + GetPrivateUserInfo get_private_user_info = 96; + GetPrivateUserInfoResponse get_private_user_info_response = 97; + UpdateHeadText update_head_text = 98; } } @@ -749,6 +751,12 @@ message Unfollow { uint32 leader_id = 2; } +message GetPrivateUserInfo {} + +message GetPrivateUserInfoResponse { + string metrics_id = 1; +} + // Entities message UpdateActiveView { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index e91a9fd558cc5baf260ebfc2e5956bb151538113..8c5832c15fbc7479f52e109d2f437cc28ee261b9 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -168,6 +168,8 @@ messages!( (UpdateWorktree, Foreground), (UpdateWorktreeExtensions, Background), (UpdateHeadText, Background), + (GetPrivateUserInfo, Foreground), + (GetPrivateUserInfoResponse, Foreground), ); request_messages!( @@ -190,6 +192,7 @@ request_messages!( (GetTypeDefinition, GetTypeDefinitionResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetReferences, GetReferencesResponse), + (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), (FuzzySearchUsers, UsersResponse), (GetUsers, UsersResponse), From 1aa554f4c98d45b7eec5646b22d65e540b4a751c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 29 Sep 2022 13:51:17 -0700 Subject: [PATCH 60/73] Fix FakeServer to expect new GetPrivateUserInfo request --- crates/client/src/test.rs | 64 ++++++++++++++------- crates/contacts_panel/src/contacts_panel.rs | 11 ++++ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index c634978a57edb3dade3bfc39e3c7ec97cf943c8a..56d3d80b631ec2adb905abbe8b0d0ed4e6f6c6d6 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Result}; use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt}; use gpui::{executor, ModelHandle, TestAppContext}; use parking_lot::Mutex; -use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope}; +use rpc::{ + proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, + ConnectionId, Peer, Receipt, TypedEnvelope, +}; use std::{fmt, rc::Rc, sync::Arc}; pub struct FakeServer { @@ -93,6 +96,7 @@ impl FakeServer { .authenticate_and_connect(false, &cx.to_async()) .await .unwrap(); + server } @@ -126,26 +130,44 @@ impl FakeServer { #[allow(clippy::await_holding_lock)] pub async fn receive(&self) -> Result> { self.executor.start_waiting(); - let message = self - .state - .lock() - .incoming - .as_mut() - .expect("not connected") - .next() - .await - .ok_or_else(|| anyhow!("other half hung up"))?; - self.executor.finish_waiting(); - let type_name = message.payload_type_name(); - Ok(*message - .into_any() - .downcast::>() - .unwrap_or_else(|_| { - panic!( - "fake server received unexpected message type: {:?}", - type_name - ); - })) + + loop { + let message = self + .state + .lock() + .incoming + .as_mut() + .expect("not connected") + .next() + .await + .ok_or_else(|| anyhow!("other half hung up"))?; + self.executor.finish_waiting(); + let type_name = message.payload_type_name(); + let message = message.into_any(); + + if message.is::>() { + return Ok(*message.downcast().unwrap()); + } + + if message.is::>() { + self.respond( + message + .downcast::>() + .unwrap() + .receipt(), + GetPrivateUserInfoResponse { + metrics_id: "the-metrics-id".into(), + }, + ) + .await; + continue; + } + + panic!( + "fake server received unexpected message type: {:?}", + type_name + ); + } } pub async fn respond( diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 7dcfb8cea48a074d86c8b35c66560a6256f42981..91b86aaf0e151daedc6094266e56212d73def107 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1220,6 +1220,17 @@ mod tests { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let server = FakeServer::for_client(current_user_id, &client, cx).await; + + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::GetPrivateUserInfoResponse { + metrics_id: "the-metrics-id".into(), + }, + ) + .await; + let fs = FakeFs::new(cx.background()); fs.insert_tree("/private_dir", json!({ "one.rs": "" })) .await; From 34926abe83a181aa547d1a9fbd8ef640d1db63f7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 29 Sep 2022 16:47:20 -0700 Subject: [PATCH 61/73] 0.57.0 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa8f8acbdc04d35462801cf208af7a3cf9918f41..8d359bf72899e0b30489b2075a6a47c22a30f369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7203,7 +7203,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.56.0" +version = "0.57.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c96163d99eb5841a1b6593079f6558a2ec8c12cc..48a84a5831930630f30dd39cb02e69599b29233b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.56.0" +version = "0.57.0" [lib] name = "zed" From fd42811ef1544d56ef4d9eb7887ab7a4ee69efb8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Sep 2022 09:51:03 +0200 Subject: [PATCH 62/73] Cache `CGEventSource` and avoid leaking `CGEvent` when handling events --- crates/gpui/src/platform/mac/event.rs | 83 +++++++++++++++------------ 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 51524f4b15742733b8e865ade79f6d7266b6fc83..ea2b492b27dec58da298f59a3b155ff4257e83dd 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -14,8 +14,10 @@ use core_graphics::{ event::{CGEvent, CGEventFlags, CGKeyCode}, event_source::{CGEventSource, CGEventSourceStateID}, }; +use ctor::ctor; +use foreign_types::ForeignType; use objc::{class, msg_send, sel, sel_impl}; -use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; +use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr}; const BACKSPACE_KEY: u16 = 0x7f; const SPACE_KEY: u16 = b' ' as u16; @@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b; const TAB_KEY: u16 = 0x09; const SHIFT_TAB_KEY: u16 = 0x19; +static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut(); + +#[ctor] +unsafe fn build_event_source() { + let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap(); + EVENT_SOURCE = source.as_ptr(); + mem::forget(source); +} + pub fn key_to_native(key: &str) -> Cow { use cocoa::appkit::*; let code = match key { @@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let mut chars_ignoring_modifiers = CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) .to_str() - .unwrap(); + .unwrap() + .to_string(); let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); let modifiers = native_event.modifierFlags(); @@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { #[allow(non_upper_case_globals)] let key = match first_char { - Some(SPACE_KEY) => "space", - Some(BACKSPACE_KEY) => "backspace", - Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", - Some(ESCAPE_KEY) => "escape", - Some(TAB_KEY) => "tab", - Some(SHIFT_TAB_KEY) => "tab", - Some(NSUpArrowFunctionKey) => "up", - Some(NSDownArrowFunctionKey) => "down", - Some(NSLeftArrowFunctionKey) => "left", - Some(NSRightArrowFunctionKey) => "right", - Some(NSPageUpFunctionKey) => "pageup", - Some(NSPageDownFunctionKey) => "pagedown", - Some(NSDeleteFunctionKey) => "delete", - Some(NSF1FunctionKey) => "f1", - Some(NSF2FunctionKey) => "f2", - Some(NSF3FunctionKey) => "f3", - Some(NSF4FunctionKey) => "f4", - Some(NSF5FunctionKey) => "f5", - Some(NSF6FunctionKey) => "f6", - Some(NSF7FunctionKey) => "f7", - Some(NSF8FunctionKey) => "f8", - Some(NSF9FunctionKey) => "f9", - Some(NSF10FunctionKey) => "f10", - Some(NSF11FunctionKey) => "f11", - Some(NSF12FunctionKey) => "f12", + Some(SPACE_KEY) => "space".to_string(), + Some(BACKSPACE_KEY) => "backspace".to_string(), + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), + Some(ESCAPE_KEY) => "escape".to_string(), + Some(TAB_KEY) => "tab".to_string(), + Some(SHIFT_TAB_KEY) => "tab".to_string(), + Some(NSUpArrowFunctionKey) => "up".to_string(), + Some(NSDownArrowFunctionKey) => "down".to_string(), + Some(NSLeftArrowFunctionKey) => "left".to_string(), + Some(NSRightArrowFunctionKey) => "right".to_string(), + Some(NSPageUpFunctionKey) => "pageup".to_string(), + Some(NSPageDownFunctionKey) => "pagedown".to_string(), + Some(NSDeleteFunctionKey) => "delete".to_string(), + Some(NSF1FunctionKey) => "f1".to_string(), + Some(NSF2FunctionKey) => "f2".to_string(), + Some(NSF3FunctionKey) => "f3".to_string(), + Some(NSF4FunctionKey) => "f4".to_string(), + Some(NSF5FunctionKey) => "f5".to_string(), + Some(NSF6FunctionKey) => "f6".to_string(), + Some(NSF7FunctionKey) => "f7".to_string(), + Some(NSF8FunctionKey) => "f8".to_string(), + Some(NSF9FunctionKey) => "f9".to_string(), + Some(NSF10FunctionKey) => "f10".to_string(), + Some(NSF11FunctionKey) => "f11".to_string(), + Some(NSF12FunctionKey) => "f12".to_string(), _ => { let mut chars_ignoring_modifiers_and_shift = chars_for_modified_key(native_event.keyCode(), false, false); @@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { shift, cmd, function, - key: key.into(), + key, } } -fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str { +fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String { // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing // an event with the given flags instead lets us access `characters`, which always // returns a valid string. - let event = CGEvent::new_keyboard_event( - CGEventSource::new(CGEventSourceStateID::Private).unwrap(), - code, - true, - ) - .unwrap(); + let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) }; + let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap(); + mem::forget(source); + let mut flags = CGEventFlags::empty(); if cmd { flags |= CGEventFlags::CGEventFlagCommand; @@ -327,10 +337,11 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a st } event.set_flags(flags); - let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] }; unsafe { + let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event]; CStr::from_ptr(event.characters().UTF8String()) .to_str() .unwrap() + .to_string() } } From 56b416202386c80fa91f3fa159ed32cd7bc3fa32 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 2 Oct 2022 18:02:25 -0700 Subject: [PATCH 63/73] Fix stray merge failure --- crates/collab/src/rpc.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 15748a52cf25acec5f9c929d4bf8d5e25a08d85b..beba653fc6dbeb5c906edff24da69fb86d045a04 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -206,11 +206,8 @@ impl Server { .add_message_handler(Server::unfollow) .add_message_handler(Server::update_followers) .add_request_handler(Server::get_channel_messages) -<<<<<<< HEAD - .add_message_handler(Server::update_head_text); -======= + .add_message_handler(Server::update_head_text) .add_request_handler(Server::get_private_user_info); ->>>>>>> 5d09083a (Identify users in amplitude via a separate 'metrics_id' UUID) Arc::new(server) } From c2370751020e52c957c93fa7ddc2cc7506dfd816 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 2 Oct 2022 18:35:19 -0700 Subject: [PATCH 64/73] Touched up settings text --- assets/settings/default.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 4ebc1e702fda87db0128d6c969dd94f364a07ea7..66cc36c38aa6994d964b77a653c70a780d35fd29 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -74,8 +74,16 @@ "hard_tabs": false, // How many columns a tab should occupy. "tab_size": 4, - // Git gutter behavior configuration. Remove this item to disable git gutters entirely. + // Git gutter behavior configuration. "git_gutter": { + // Which files to show the git gutter on. This setting can take + // three values: + // 1. All files: + // "files_included": "all", + // 2. Only files tracked in git: + // "files_included": "only_tracked", + // 3. Disable git gutters: + // "files_included": "none", "files_included": "all" }, // Settings specific to the terminal From 01176e04b7f0399eb7e6f62bfbe7019cd0f39545 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 2 Oct 2022 18:42:03 -0700 Subject: [PATCH 65/73] Added clarification for git gutter settings --- assets/settings/default.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 66cc36c38aa6994d964b77a653c70a780d35fd29..11a4b72a108e313aa5bb154a1b85e1141fbf4629 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -78,11 +78,12 @@ "git_gutter": { // Which files to show the git gutter on. This setting can take // three values: - // 1. All files: + // 1. All files, files not tracked in git will be diffed against + // their contents when the file was last opened in Zed: // "files_included": "all", - // 2. Only files tracked in git: + // 2. Only show for files tracked in git: // "files_included": "only_tracked", - // 3. Disable git gutters: + // 3. Disable git gutters entirely: // "files_included": "none", "files_included": "all" }, From 9427bb7553ea5eb7bd51cdc6a94ab570300f624e Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Oct 2022 11:58:48 -0400 Subject: [PATCH 66/73] Be clearer about using GitFilesIncluded setting --- crates/project/src/worktree.rs | 39 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 40efeee1d1586f40ee18bd85dda50029596c74fe..c650111207ed7634771c0c314bc766df5d9ff842 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -673,28 +673,27 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let head_text = if matches!( - files_included, - settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked - ) { - let results = if let Some(repo) = snapshot.repo_for(&abs_path) { - cx.background() - .spawn({ - let path = path.clone(); - async move { repo.repo.lock().load_head_text(&path) } - }) - .await - } else { - None - }; + let head_text = match files_included { + settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked => { + let results = if let Some(repo) = snapshot.repo_for(&abs_path) { + cx.background() + .spawn({ + let path = path.clone(); + async move { repo.repo.lock().load_head_text(&path) } + }) + .await + } else { + None + }; - if files_included == settings::GitFilesIncluded::All { - results.or_else(|| Some(text.clone())) - } else { - results + if files_included == settings::GitFilesIncluded::All { + results.or_else(|| Some(text.clone())) + } else { + results + } } - } else { - None + + settings::GitFilesIncluded::None => None, }; // Eagerly populate the snapshot with an updated entry for the loaded file From 8f4b3c34938acce79e360266a914a31b64d47480 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Oct 2022 14:00:58 -0400 Subject: [PATCH 67/73] Store repo content path as absolute Co-Authored-By: Mikayla Maki --- crates/project/src/worktree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c650111207ed7634771c0c314bc766df5d9ff842..e04ff2b516b8295802aaa75d51d19a5f4fb1045c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1561,7 +1561,7 @@ impl LocalSnapshot { if parent_path.file_name() == Some(&DOT_GIT) { let abs_path = self.abs_path.join(&parent_path); - let content_path: Arc = parent_path.parent().unwrap().into(); + let content_path: Arc = abs_path.parent().unwrap().into(); if let Err(ix) = self .git_repositories .binary_search_by_key(&&content_path, |repo| &repo.content_path) From a5c2f22bf7339066f7e507c059febe1884a500f5 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Oct 2022 14:53:33 -0400 Subject: [PATCH 68/73] Move git gutter settings out of editor settings Co-Authored-By: Mikayla Maki --- assets/settings/default.json | 24 +++++++++++----------- crates/collab/src/rpc.rs | 2 +- crates/project/src/worktree.rs | 4 ++-- crates/settings/src/settings.rs | 33 +++++++++++++++++++------------ crates/workspace/src/workspace.rs | 4 ++-- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 11a4b72a108e313aa5bb154a1b85e1141fbf4629..fc1b1906fcb8dc3e144177fb1c511dc0d39d6f22 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -75,17 +75,19 @@ // How many columns a tab should occupy. "tab_size": 4, // Git gutter behavior configuration. - "git_gutter": { - // Which files to show the git gutter on. This setting can take - // three values: - // 1. All files, files not tracked in git will be diffed against - // their contents when the file was last opened in Zed: - // "files_included": "all", - // 2. Only show for files tracked in git: - // "files_included": "only_tracked", - // 3. Disable git gutters entirely: - // "files_included": "none", - "files_included": "all" + "git": { + "git_gutter": { + // Which files to show the git gutter on. This setting can take + // three values: + // 1. All files, files not tracked in git will be diffed against + // their contents when the file was last opened in Zed: + // "files_included": "all", + // 2. Only show for files tracked in git: + // "files_included": "only_tracked", + // 3. Disable git gutters entirely: + // "files_included": "none", + "files_included": "all" + } }, // Settings specific to the terminal "terminal": { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 627eaf719e7c06df0802394a91129ecd8a8524f6..609ae89625db1a9e41958c1ae4cdb2721cd16584 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1744,7 +1744,7 @@ impl Server { Ok(()) } - async fn get_private_user_info( + async fn get_private_user_info( self: Arc, request: TypedEnvelope, response: Response, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e04ff2b516b8295802aaa75d51d19a5f4fb1045c..b914282e0186a5e02ca75eab36fca511b8dcbc8a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -665,9 +665,9 @@ impl LocalWorktree { let files_included = cx .global::() - .editor_overrides + .git .git_gutter - .unwrap_or_default() + .expect("This should be Some by setting setup") .files_included; cx.spawn(|this, mut cx| async move { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 3f4a764c79641e268f06d6ab78939577910856bc..9de4335ec8f3f8cec0dbefa80c5b603f2456e7ca 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -32,6 +32,7 @@ pub struct Settings { pub default_dock_anchor: DockAnchor, pub editor_defaults: EditorSettings, pub editor_overrides: EditorSettings, + pub git: GitSettings, pub terminal_defaults: TerminalSettings, pub terminal_overrides: TerminalSettings, pub language_defaults: HashMap, EditorSettings>, @@ -52,20 +53,13 @@ impl FeatureFlags { } } -#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] -pub struct EditorSettings { - pub tab_size: Option, - pub hard_tabs: Option, - pub soft_wrap: Option, - pub preferred_line_length: Option, - pub format_on_save: Option, - pub formatter: Option, - pub enable_language_server: Option, - pub git_gutter: Option, +#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, } #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] -pub struct GitGutterConfig { +pub struct GitGutterSettings { pub files_included: GitFilesIncluded, pub debounce_delay_millis: Option, } @@ -79,6 +73,17 @@ pub enum GitFilesIncluded { None, } +#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] +pub struct EditorSettings { + pub tab_size: Option, + pub hard_tabs: Option, + pub soft_wrap: Option, + pub preferred_line_length: Option, + pub format_on_save: Option, + pub formatter: Option, + pub enable_language_server: Option, +} + #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { @@ -212,6 +217,8 @@ pub struct SettingsFileContent { #[serde(default)] pub terminal: TerminalSettings, #[serde(default)] + pub git: Option, + #[serde(default)] #[serde(alias = "language_overrides")] pub languages: HashMap, EditorSettings>, #[serde(default)] @@ -266,9 +273,9 @@ impl Settings { format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), - git_gutter: defaults.editor.git_gutter, }, editor_overrides: Default::default(), + git: defaults.git.unwrap(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), language_defaults: defaults.languages, @@ -395,11 +402,11 @@ impl Settings { format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), - git_gutter: Default::default(), }, editor_overrides: Default::default(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), + git: Default::default(), language_defaults: Default::default(), language_overrides: Default::default(), lsp: Default::default(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 921fb2de201e4c1235a47239543de5d6f0d8bd71..fc1f6432a195dd5f0e573f0bc234670a0e1f6813 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -736,9 +736,9 @@ impl ItemHandle for ViewHandle { let debounce_delay = cx .global::() - .editor_overrides + .git .git_gutter - .unwrap_or_default() + .expect("This should be Some by setting setup") .debounce_delay_millis; let item = item.clone(); From e6487de0691ffcafa1e727727bddfec4dc2d6377 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Oct 2022 15:11:06 -0400 Subject: [PATCH 69/73] Rename head text to indicate that it's not always going to be from head Co-Authored-By: Mikayla Maki --- crates/collab/src/integration_tests.rs | 30 +++++++++++++------------- crates/collab/src/rpc.rs | 6 +++--- crates/git/src/diff.rs | 24 ++++++++++----------- crates/git/src/repository.rs | 9 ++++---- crates/language/src/buffer.rs | 28 ++++++++++++------------ crates/project/src/fs.rs | 6 +----- crates/project/src/project.rs | 20 ++++++++--------- crates/project/src/worktree.rs | 10 ++++----- crates/rpc/proto/zed.proto | 8 +++---- crates/rpc/src/proto.rs | 4 ++-- crates/settings/src/settings.rs | 11 +++++++--- 11 files changed, 78 insertions(+), 78 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a13a013e7a4f624af7c0dea1fe7d11970962bfaa..58a8efc411f739e73d3b32c233902cd07ea2cbc4 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -948,7 +948,7 @@ async fn test_propagate_saves_and_fs_changes( } #[gpui::test(iterations = 10)] -async fn test_git_head_text( +async fn test_git_diff_base_change( executor: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -977,13 +977,13 @@ async fn test_git_head_text( ) .await; - let head_text = " + let diff_base = " one three " .unindent(); - let new_head_text = " + let new_diff_base = " one two " @@ -992,9 +992,9 @@ async fn test_git_head_text( client_a .fs .as_fake() - .set_head_state_for_git_repository( + .set_index_for_repo( Path::new("/dir/.git"), - &[(Path::new("a.txt"), head_text.clone())], + &[(Path::new("a.txt"), diff_base.clone())], ) .await; @@ -1012,11 +1012,11 @@ async fn test_git_head_text( // Smoke test diffing buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.head_text(), Some(head_text.as_ref())); + assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, - &head_text, + &diff_base, &[(1..2, "", "two\n")], ); }); @@ -1032,11 +1032,11 @@ async fn test_git_head_text( // Smoke test diffing buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.head_text(), Some(head_text.as_ref())); + assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, - &head_text, + &diff_base, &[(1..2, "", "two\n")], ); }); @@ -1044,9 +1044,9 @@ async fn test_git_head_text( client_a .fs .as_fake() - .set_head_state_for_git_repository( + .set_index_for_repo( Path::new("/dir/.git"), - &[(Path::new("a.txt"), new_head_text.clone())], + &[(Path::new("a.txt"), new_diff_base.clone())], ) .await; @@ -1055,23 +1055,23 @@ async fn test_git_head_text( // Smoke test new diffing buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); + assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, - &head_text, + &diff_base, &[(2..3, "", "three\n")], ); }); // Smoke test B buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.head_text(), Some(new_head_text.as_ref())); + assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( buffer.snapshot().git_diff_hunks_in_range(0..4), &buffer, - &head_text, + &diff_base, &[(2..3, "", "three\n")], ); }); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 609ae89625db1a9e41958c1ae4cdb2721cd16584..9f3c01ac83a0bc8289c696fc5b0e30927ed34066 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -206,7 +206,7 @@ impl Server { .add_message_handler(Server::unfollow) .add_message_handler(Server::update_followers) .add_request_handler(Server::get_channel_messages) - .add_message_handler(Server::update_head_text) + .add_message_handler(Server::update_diff_base) .add_request_handler(Server::get_private_user_info); Arc::new(server) @@ -1729,9 +1729,9 @@ impl Server { Ok(()) } - async fn update_head_text( + async fn update_diff_base( self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self.store().await.project_connection_ids( ProjectId::from_proto(request.payload.project_id), diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 48630fc91cbd0d6663b6f506deb4b1fd23ec593d..abf874e2bb149659b66a1c81c731a6e9b2bae91d 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -111,11 +111,11 @@ impl BufferDiff { } } - pub async fn update(&mut self, head_text: &str, buffer: &text::BufferSnapshot) { + pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) { let mut tree = SumTree::new(); let buffer_text = buffer.as_rope().to_string(); - let patch = Self::diff(&head_text, &buffer_text); + let patch = Self::diff(&diff_base, &buffer_text); if let Some(patch) = patch { let mut divergence = 0; @@ -228,7 +228,7 @@ impl BufferDiff { pub fn assert_hunks( diff_hunks: Iter, buffer: &BufferSnapshot, - head_text: &str, + diff_base: &str, expected_hunks: &[(Range, &str, &str)], ) where Iter: Iterator>, @@ -237,7 +237,7 @@ pub fn assert_hunks( .map(|hunk| { ( hunk.buffer_range.clone(), - &head_text[hunk.head_byte_range], + &diff_base[hunk.head_byte_range], buffer .text_for_range( Point::new(hunk.buffer_range.start, 0) @@ -264,7 +264,7 @@ mod tests { #[test] fn test_buffer_diff_simple() { - let head_text = " + let diff_base = " one two three @@ -280,27 +280,27 @@ mod tests { let mut buffer = Buffer::new(0, 0, buffer_text); let mut diff = BufferDiff::new(); - smol::block_on(diff.update(&head_text, &buffer)); + smol::block_on(diff.update(&diff_base, &buffer)); assert_hunks( diff.hunks(&buffer), &buffer, - &head_text, + &diff_base, &[(1..2, "two\n", "HELLO\n")], ); buffer.edit([(0..0, "point five\n")]); - smol::block_on(diff.update(&head_text, &buffer)); + smol::block_on(diff.update(&diff_base, &buffer)); assert_hunks( diff.hunks(&buffer), &buffer, - &head_text, + &diff_base, &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], ); } #[test] fn test_buffer_diff_range() { - let head_text = " + let diff_base = " one two three @@ -337,13 +337,13 @@ mod tests { let buffer = Buffer::new(0, 0, buffer_text); let mut diff = BufferDiff::new(); - smol::block_on(diff.update(&head_text, &buffer)); + smol::block_on(diff.update(&diff_base, &buffer)); assert_eq!(diff.hunks(&buffer).count(), 8); assert_hunks( diff.hunks_in_range(7..12, &buffer), &buffer, - &head_text, + &diff_base, &[ (6..7, "", "HELLO\n"), (9..10, "six\n", "SIXTEEN\n"), diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a49a1e0b600484d094f5c19b86adfd6cf1e3631e..67e93416aebb2caa8b3b3d6611a8a5637b0e7ba5 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -10,12 +10,12 @@ pub use git2::Repository as LibGitRepository; #[async_trait::async_trait] pub trait GitRepository: Send { - fn load_head_text(&self, relative_file_path: &Path) -> Option; + fn load_index(&self, relative_file_path: &Path) -> Option; } #[async_trait::async_trait] impl GitRepository for LibGitRepository { - fn load_head_text(&self, relative_file_path: &Path) -> Option { + fn load_index(&self, relative_file_path: &Path) -> Option { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; @@ -25,8 +25,7 @@ impl GitRepository for LibGitRepository { }; let content = repo.find_blob(oid)?.content().to_owned(); - let head_text = String::from_utf8(content)?; - Ok(Some(head_text)) + Ok(Some(String::from_utf8(content)?)) } match logic(&self, relative_file_path) { @@ -55,7 +54,7 @@ impl FakeGitRepository { #[async_trait::async_trait] impl GitRepository for FakeGitRepository { - fn load_head_text(&self, path: &Path) -> Option { + fn load_index(&self, path: &Path) -> Option { let state = self.state.lock(); state.index_contents.get(path).cloned() } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 22706ab1b59c04919839746ac6b6565280cbe54e..11ca4fa52ae5d3cf0a66022be66c398a3d6d893f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -53,7 +53,7 @@ struct GitDiffStatus { pub struct Buffer { text: TextBuffer, - head_text: Option, + diff_base: Option, git_diff_status: GitDiffStatus, file: Option>, saved_version: clock::Global, @@ -346,13 +346,13 @@ impl Buffer { pub fn from_file>( replica_id: ReplicaId, base_text: T, - head_text: Option, + diff_base: 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().into_boxed_str().into()), + diff_base.map(|h| h.into().into_boxed_str().into()), Some(file), ) } @@ -365,7 +365,7 @@ impl Buffer { let buffer = TextBuffer::new(replica_id, message.id, message.base_text); let mut this = Self::build( buffer, - message.head_text.map(|text| text.into_boxed_str().into()), + message.diff_base.map(|text| text.into_boxed_str().into()), file, ); this.text.set_line_ending(proto::deserialize_line_ending( @@ -380,7 +380,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.as_ref().map(|h| h.to_string()), + diff_base: self.diff_base.as_ref().map(|h| h.to_string()), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, } } @@ -423,7 +423,7 @@ impl Buffer { self } - fn build(buffer: TextBuffer, head_text: Option, file: Option>) -> Self { + fn build(buffer: TextBuffer, diff_base: Option, file: Option>) -> Self { let saved_mtime = if let Some(file) = file.as_ref() { file.mtime() } else { @@ -437,7 +437,7 @@ impl Buffer { transaction_depth: 0, was_dirty_before_starting_transaction: None, text: buffer, - head_text, + diff_base, git_diff_status: GitDiffStatus { diff: git::diff::BufferDiff::new(), update_in_progress: false, @@ -663,12 +663,12 @@ impl Buffer { } #[cfg(any(test, feature = "test-support"))] - pub fn head_text(&self) -> Option<&str> { - self.head_text.as_deref() + pub fn diff_base(&self) -> Option<&str> { + self.diff_base.as_deref() } - pub fn update_head_text(&mut self, head_text: Option, cx: &mut ModelContext) { - self.head_text = head_text; + pub fn update_diff_base(&mut self, diff_base: Option, cx: &mut ModelContext) { + self.diff_base = diff_base; self.git_diff_recalc(cx); } @@ -682,13 +682,13 @@ impl Buffer { return; } - if let Some(head_text) = &self.head_text { + if let Some(diff_base) = &self.diff_base { let snapshot = self.snapshot(); - let head_text = head_text.clone(); + let diff_base = diff_base.clone(); let mut diff = self.git_diff_status.diff.clone(); let diff = cx.background().spawn(async move { - diff.update(&head_text, &snapshot).await; + diff.update(&diff_base, &snapshot).await; diff }); diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 2b7aca642da2ad442b66afb496b12f20add31949..a43f18ca640c68da051e1c58008d4861921d1ca6 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -490,11 +490,7 @@ impl FakeFs { .boxed() } - pub async fn set_head_state_for_git_repository( - &self, - dot_git: &Path, - head_state: &[(&Path, String)], - ) { + pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) { let content_path = dot_git.parent().unwrap(); let mut state = self.state.lock().await; let entry = state.read_path(dot_git).await.unwrap(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1064d05fe9a6610617baa21226f2f30d552fce17..7ce9b4608597a82f0e2c27d86092e0db206aea9e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -424,7 +424,7 @@ impl Project { client.add_model_request_handler(Self::handle_open_buffer_by_id); client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_save_buffer); - client.add_model_message_handler(Self::handle_update_head_text); + client.add_model_message_handler(Self::handle_update_diff_base); } pub fn local( @@ -4675,22 +4675,22 @@ impl Project { let client = self.client.clone(); cx.spawn(|_, mut cx| async move { - let head_text = cx + let diff_base = cx .background() - .spawn(async move { repo.repo.lock().load_head_text(&path) }) + .spawn(async move { repo.repo.lock().load_index(&path) }) .await; let buffer_id = buffer.update(&mut cx, |buffer, cx| { - buffer.update_head_text(head_text.clone(), cx); + buffer.update_diff_base(diff_base.clone(), cx); buffer.remote_id() }); if let Some(project_id) = shared_remote_id { client - .send(proto::UpdateHeadText { + .send(proto::UpdateDiffBase { project_id, buffer_id: buffer_id as u64, - head_text, + diff_base, }) .log_err(); } @@ -5272,22 +5272,22 @@ impl Project { }) } - async fn handle_update_head_text( + async fn handle_update_diff_base( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, cx| { let buffer_id = envelope.payload.buffer_id; - let head_text = envelope.payload.head_text; + let diff_base = envelope.payload.diff_base; let buffer = this .opened_buffers .get_mut(&buffer_id) .and_then(|b| b.upgrade(cx)) .ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?; - buffer.update(cx, |buffer, cx| buffer.update_head_text(head_text, cx)); + buffer.update(cx, |buffer, cx| buffer.update_diff_base(diff_base, cx)); Ok(()) }) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b914282e0186a5e02ca75eab36fca511b8dcbc8a..ea02431ab9da45861f961e9867773d70899e165e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -481,11 +481,11 @@ impl LocalWorktree { ) -> Task>> { let path = Arc::from(path); cx.spawn(move |this, mut cx| async move { - let (file, contents, head_text) = this + let (file, contents, diff_base) = this .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) .await?; Ok(cx.add_model(|cx| { - let mut buffer = Buffer::from_file(0, contents, head_text, Arc::new(file), cx); + let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx); buffer.git_diff_recalc(cx); buffer })) @@ -673,13 +673,13 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let head_text = match files_included { + let diff_base = match files_included { settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked => { let results = if let Some(repo) = snapshot.repo_for(&abs_path) { cx.background() .spawn({ let path = path.clone(); - async move { repo.repo.lock().load_head_text(&path) } + async move { repo.repo.lock().load_index(&path) } }) .await } else { @@ -714,7 +714,7 @@ impl LocalWorktree { is_local: true, }, text, - head_text, + diff_base, )) }) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 832c5bb6bd67fb5133e120b93510862b61563315..3c7fa2ad40da21d85a5cc18617eb27b0327f653d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -110,7 +110,7 @@ message Envelope { Unfollow unfollow = 95; GetPrivateUserInfo get_private_user_info = 96; GetPrivateUserInfoResponse get_private_user_info_response = 97; - UpdateHeadText update_head_text = 98; + UpdateDiffBase update_diff_base = 98; } } @@ -830,7 +830,7 @@ message BufferState { uint64 id = 1; optional File file = 2; string base_text = 3; - optional string head_text = 4; + optional string diff_base = 4; LineEnding line_ending = 5; } @@ -1002,8 +1002,8 @@ message WorktreeMetadata { bool visible = 3; } -message UpdateHeadText { +message UpdateDiffBase { uint64 project_id = 1; uint64 buffer_id = 2; - optional string head_text = 3; + optional string diff_base = 3; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8c5832c15fbc7479f52e109d2f437cc28ee261b9..8d9d715b6c37739f03530fd69f9a5c2f9f6d4a5e 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -167,7 +167,7 @@ messages!( (UpdateProject, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeExtensions, Background), - (UpdateHeadText, Background), + (UpdateDiffBase, Background), (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), ); @@ -267,7 +267,7 @@ entity_messages!( UpdateProject, UpdateWorktree, UpdateWorktreeExtensions, - UpdateHeadText + UpdateDiffBase ); entity_messages!(channel_id, ChannelMessageSent); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 9de4335ec8f3f8cec0dbefa80c5b603f2456e7ca..96555297440c5f91363b05121c97eb3e23ece44f 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -55,11 +55,11 @@ impl FeatureFlags { #[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)] pub struct GitSettings { - pub git_gutter: Option, + pub git_gutter: Option, } #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] -pub struct GitGutterSettings { +pub struct GitGutter { pub files_included: GitFilesIncluded, pub debounce_delay_millis: Option, } @@ -406,7 +406,12 @@ impl Settings { editor_overrides: Default::default(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), - git: Default::default(), + git: GitSettings { + git_gutter: Some(GitGutter { + files_included: GitFilesIncluded::All, + debounce_delay_millis: None, + }), + }, language_defaults: Default::default(), language_overrides: Default::default(), lsp: Default::default(), From 6f6d72890a3981140e2258cad67ec9113d358f35 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Oct 2022 15:42:30 -0400 Subject: [PATCH 70/73] Once again respect user settings for git gutter Co-Authored-By: Mikayla Maki --- crates/project/src/worktree.rs | 8 ++------ crates/settings/src/settings.rs | 27 +++++++++++++++++++++++++-- crates/workspace/src/workspace.rs | 14 ++++++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ea02431ab9da45861f961e9867773d70899e165e..1016e58b739805b1e25c853b7c185b1301e366e5 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -663,12 +663,8 @@ impl LocalWorktree { let fs = self.fs.clone(); let snapshot = self.snapshot(); - let files_included = cx - .global::() - .git - .git_gutter - .expect("This should be Some by setting setup") - .files_included; + let settings = cx.global::(); + let files_included = settings.git_gutter().files_included(settings); cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 96555297440c5f91363b05121c97eb3e23ece44f..3bf09436edd81ed5bf83affd0a81ab0ff9ff359c 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -33,6 +33,7 @@ pub struct Settings { pub editor_defaults: EditorSettings, pub editor_overrides: EditorSettings, pub git: GitSettings, + pub git_overrides: GitSettings, pub terminal_defaults: TerminalSettings, pub terminal_overrides: TerminalSettings, pub language_defaults: HashMap, EditorSettings>, @@ -60,10 +61,21 @@ pub struct GitSettings { #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct GitGutter { - pub files_included: GitFilesIncluded, + pub files_included: Option, pub debounce_delay_millis: Option, } +impl GitGutter { + pub fn files_included(&self, settings: &Settings) -> GitFilesIncluded { + self.files_included.unwrap_or_else(|| { + settings + .git.git_gutter.expect("git_gutter must be some in defaults.json") + .files_included + .expect("Should be some in defaults.json") + }) + } +} + #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum GitFilesIncluded { @@ -276,6 +288,7 @@ impl Settings { }, editor_overrides: Default::default(), git: defaults.git.unwrap(), + git_overrides: Default::default(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), language_defaults: defaults.languages, @@ -327,6 +340,7 @@ impl Settings { } self.editor_overrides = data.editor; + self.git_overrides = data.git.unwrap_or_default(); self.terminal_defaults.font_size = data.terminal.font_size; self.terminal_overrides = data.terminal; self.language_overrides = data.languages; @@ -382,6 +396,14 @@ impl Settings { .expect("missing default") } + pub fn git_gutter(&self) -> GitGutter { + self.git_overrides.git_gutter.unwrap_or_else(|| { + self.git + .git_gutter + .expect("git_gutter should be some by setting setup") + }) + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { @@ -408,10 +430,11 @@ impl Settings { terminal_overrides: Default::default(), git: GitSettings { git_gutter: Some(GitGutter { - files_included: GitFilesIncluded::All, + files_included: Some(GitFilesIncluded::All), debounce_delay_millis: None, }), }, + git_overrides: Default::default(), language_defaults: Default::default(), language_overrides: Default::default(), lsp: Default::default(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fc1f6432a195dd5f0e573f0bc234670a0e1f6813..44c9b19f1bbd419b6e6eceb3b3c39c31fc587bec 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -734,12 +734,18 @@ impl ItemHandle for ViewHandle { ); } - let debounce_delay = cx - .global::() - .git + let settings = cx.global::(); + let debounce_delay = settings + .git_overrides .git_gutter - .expect("This should be Some by setting setup") + .unwrap_or_else(|| { + settings + .git + .git_gutter + .expect("This should be Some by setting setup") + }) .debounce_delay_millis; + let item = item.clone(); if let Some(delay) = debounce_delay { From 6f7547d28f131cf1af5ef59b593d9f813c0a6786 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 3 Oct 2022 17:18:38 -0700 Subject: [PATCH 71/73] Fixed a couple bugs in tests and worktree path handling --- crates/git/src/repository.rs | 1 + crates/project/src/fs.rs | 3 +-- crates/project/src/project.rs | 16 ++++++++------ crates/project/src/worktree.rs | 39 ++++++++++++++++------------------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 67e93416aebb2caa8b3b3d6611a8a5637b0e7ba5..38393dc8a8c4b143e87dc27d68e3e87811696f04 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -19,6 +19,7 @@ impl GitRepository for LibGitRepository { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; + dbg!(relative_file_path); let oid = match index.get_path(relative_file_path, STAGE_NORMAL) { Some(entry) => entry.id, None => return Ok(None), diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index a43f18ca640c68da051e1c58008d4861921d1ca6..812842a354c10f9ec4e035edd8ec5c1744a0f6db 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -888,8 +888,7 @@ impl Fs for FakeFs { } fn open_repo(&self, abs_dot_git: &Path) -> Option>> { - let executor = self.executor.upgrade().unwrap(); - executor.block(async move { + smol::block_on(async move { let state = self.state.lock().await; let entry = state.read_path(abs_dot_git).await.unwrap(); let mut entry = entry.lock().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7ce9b4608597a82f0e2c27d86092e0db206aea9e..dc783f181834cb074867c5096f6f818c4ad95f2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4541,7 +4541,7 @@ impl Project { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), worktree::Event::UpdatedGitRepositories(updated_repos) => { - this.update_local_worktree_buffers_git_repos(updated_repos, cx) + this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) } }) .detach(); @@ -4652,21 +4652,23 @@ impl Project { fn update_local_worktree_buffers_git_repos( &mut self, + worktree: ModelHandle, repos: &[GitRepositoryEntry], cx: &mut ModelContext, ) { - //TODO: Produce protos - for (_, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { - let file = match buffer.read(cx).file().and_then(|file| file.as_local()) { + let file = match File::from_dyn(buffer.read(cx).file()) { Some(file) => file, - None => return, + None => continue, }; + if file.worktree != worktree { + continue; + } + let path = file.path().clone(); - let abs_path = file.abs_path(cx); - let repo = match repos.iter().find(|repo| repo.manages(&abs_path)) { + let repo = match repos.iter().find(|repo| repo.manages(&path)) { Some(repo) => repo.clone(), None => return, }; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1016e58b739805b1e25c853b7c185b1301e366e5..fb07bd837f3ed1ce1e8a2a8cc30c7bf7fa9f537a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -41,6 +41,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, + mem, ops::{Deref, DerefMut}, os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, @@ -664,6 +665,13 @@ impl LocalWorktree { let snapshot = self.snapshot(); let settings = cx.global::(); + + // Cut files included because we want to ship! + // TODO: + // - Rename / etc. setting to be show/hide git gutters + // - Unconditionally load index text for all files, + // - then choose at rendering time based on settings + let files_included = settings.git_gutter().files_included(settings); cx.spawn(|this, mut cx| async move { @@ -1379,6 +1387,7 @@ impl LocalSnapshot { // Gives the most specific git repository for a given path pub(crate) fn repo_for(&self, path: &Path) -> Option { + dbg!(&self.git_repositories); self.git_repositories .iter() .rev() //git_repository is ordered lexicographically @@ -1557,7 +1566,7 @@ impl LocalSnapshot { if parent_path.file_name() == Some(&DOT_GIT) { let abs_path = self.abs_path.join(&parent_path); - let content_path: Arc = abs_path.parent().unwrap().into(); + let content_path: Arc = parent_path.parent().unwrap().into(); if let Err(ix) = self .git_repositories .binary_search_by_key(&&content_path, |repo| &repo.content_path) @@ -1716,6 +1725,7 @@ impl LocalSnapshot { impl GitRepositoryEntry { // Note that these paths should be relative to the worktree root. pub(crate) fn manages(&self, path: &Path) -> bool { + dbg!(path, &self.content_path); path.starts_with(self.content_path.as_ref()) } @@ -2566,7 +2576,7 @@ impl BackgroundScanner { self.snapshot.lock().removed_entry_ids.clear(); self.update_ignore_statuses().await; - self.update_git_repositories().await; + self.update_git_repositories(); true } @@ -2632,25 +2642,11 @@ impl BackgroundScanner { .await; } - // TODO: Clarify what is going on here because re-loading every git repository - // on every file system event seems wrong - async fn update_git_repositories(&self) { + fn update_git_repositories(&self) { let mut snapshot = self.snapshot.lock(); - - let new_repos = snapshot - .git_repositories - .iter() - .cloned() - .filter_map(|mut repo_entry| { - let repo = self - .fs - .open_repo(&snapshot.abs_path.join(&repo_entry.git_dir_path))?; - repo_entry.repo = repo; - Some(repo_entry) - }) - .collect(); - - snapshot.git_repositories = new_repos; + let mut git_repositories = mem::take(&mut snapshot.git_repositories); + git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some()); + snapshot.git_repositories = git_repositories; } async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { @@ -3245,7 +3241,8 @@ mod tests { "b.txt": "" } }, - "c.txt": "" + "c.txt": "", + })); let http_client = FakeHttpClient::with_404_response(); From 499e95d16a1213ab89052a98d0b3bd5e9b297f3e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 3 Oct 2022 17:43:05 -0700 Subject: [PATCH 72/73] Removed debugs, simplified settings --- assets/settings/default.json | 18 +++----- crates/editor/src/element.rs | 73 ++++++++++++++++--------------- crates/git/src/repository.rs | 1 - crates/project/src/worktree.rs | 44 +++++-------------- crates/settings/src/settings.rs | 34 +++----------- crates/workspace/src/workspace.rs | 11 +---- 6 files changed, 62 insertions(+), 119 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index fc1b1906fcb8dc3e144177fb1c511dc0d39d6f22..fddac662a5be681b9e6785ac9a93f4712bcb9bd3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -76,18 +76,12 @@ "tab_size": 4, // Git gutter behavior configuration. "git": { - "git_gutter": { - // Which files to show the git gutter on. This setting can take - // three values: - // 1. All files, files not tracked in git will be diffed against - // their contents when the file was last opened in Zed: - // "files_included": "all", - // 2. Only show for files tracked in git: - // "files_included": "only_tracked", - // 3. Disable git gutters entirely: - // "files_included": "none", - "files_included": "all" - } + // Control whether the git gutter is shown. May take 2 values: + // 1. Show the gutter + // "git_gutter": "tracked_files" + // 2. Hide the gutter + // "git_gutter": "hide" + "git_gutter": "tracked_files" }, // Settings specific to the terminal "terminal": { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5d83051567b097092e9cd45804eba50c69fddce6..56887f4b45600a8b5c50feb97bff3f94170b2cfb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -37,7 +37,7 @@ use gpui::{ use json::json; use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use project::ProjectPath; -use settings::Settings; +use settings::{GitGutter, Settings}; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, @@ -607,13 +607,16 @@ impl EditorElement { }; let diff_style = &cx.global::().theme.editor.diff.clone(); - // dbg!("***************"); - // dbg!(&layout.diff_hunks); - // dbg!("***************"); + let show_gutter = matches!( + &cx.global::() + .git_overrides + .git_gutter + .unwrap_or_default(), + GitGutter::TrackedFiles + ); // line is `None` when there's a line wrap for (ix, line) in layout.line_number_layouts.iter().enumerate() { - // dbg!(ix); if let Some(line) = line { let line_origin = bounds.origin() + vec2f( @@ -624,39 +627,39 @@ impl EditorElement { line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx); - //This line starts a buffer line, so let's do the diff calculation - let new_hunk = get_hunk(diff_layout.buffer_line, &layout.diff_hunks); - - // This + the unwraps are annoying, but at least it's legible - let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) { - (None, None) => (false, false), - (None, Some(_)) => (false, true), - (Some(_), None) => (true, false), - (Some((old_hunk, _)), Some(new_hunk)) if new_hunk == old_hunk => (false, false), - (Some(_), Some(_)) => (true, true), - }; - - // dbg!(diff_layout.buffer_line, is_starting); - - if is_ending { - let (last_hunk, start_line) = diff_layout.last_diff.take().unwrap(); - // dbg!("ending"); - // dbg!(start_line..ix); - cx.scene.push_quad(diff_quad( - last_hunk.status(), - start_line..ix, - &gutter_layout, - diff_style, - )); - } + if show_gutter { + //This line starts a buffer line, so let's do the diff calculation + let new_hunk = get_hunk(diff_layout.buffer_line, &layout.diff_hunks); + + // This + the unwraps are annoying, but at least it's legible + let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) { + (None, None) => (false, false), + (None, Some(_)) => (false, true), + (Some(_), None) => (true, false), + (Some((old_hunk, _)), Some(new_hunk)) if new_hunk == old_hunk => { + (false, false) + } + (Some(_), Some(_)) => (true, true), + }; - if is_starting { - let new_hunk = new_hunk.unwrap(); + if is_ending { + let (last_hunk, start_line) = diff_layout.last_diff.take().unwrap(); + cx.scene.push_quad(diff_quad( + last_hunk.status(), + start_line..ix, + &gutter_layout, + diff_style, + )); + } + + if is_starting { + let new_hunk = new_hunk.unwrap(); - diff_layout.last_diff = Some((new_hunk, ix)); - }; + diff_layout.last_diff = Some((new_hunk, ix)); + }; - diff_layout.buffer_line += 1; + diff_layout.buffer_line += 1; + } } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 38393dc8a8c4b143e87dc27d68e3e87811696f04..67e93416aebb2caa8b3b3d6611a8a5637b0e7ba5 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -19,7 +19,6 @@ impl GitRepository for LibGitRepository { fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; - dbg!(relative_file_path); let oid = match index.get_path(relative_file_path, STAGE_NORMAL) { Some(entry) => entry.id, None => return Ok(None), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fb07bd837f3ed1ce1e8a2a8cc30c7bf7fa9f537a..6880ec4ff11c1ac4df4ffb7e0c163626e0fc11a8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -32,7 +32,7 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; -use settings::Settings; + use smol::channel::{self, Sender}; use std::{ any::Any, @@ -664,40 +664,18 @@ impl LocalWorktree { let fs = self.fs.clone(); let snapshot = self.snapshot(); - let settings = cx.global::(); - - // Cut files included because we want to ship! - // TODO: - // - Rename / etc. setting to be show/hide git gutters - // - Unconditionally load index text for all files, - // - then choose at rendering time based on settings - - let files_included = settings.git_gutter().files_included(settings); - cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let diff_base = match files_included { - settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked => { - let results = if let Some(repo) = snapshot.repo_for(&abs_path) { - cx.background() - .spawn({ - let path = path.clone(); - async move { repo.repo.lock().load_index(&path) } - }) - .await - } else { - None - }; - - if files_included == settings::GitFilesIncluded::All { - results.or_else(|| Some(text.clone())) - } else { - results - } - } - - settings::GitFilesIncluded::None => None, + let diff_base = if let Some(repo) = snapshot.repo_for(&abs_path) { + cx.background() + .spawn({ + let path = path.clone(); + async move { repo.repo.lock().load_index(&path) } + }) + .await + } else { + None }; // Eagerly populate the snapshot with an updated entry for the loaded file @@ -1387,7 +1365,6 @@ impl LocalSnapshot { // Gives the most specific git repository for a given path pub(crate) fn repo_for(&self, path: &Path) -> Option { - dbg!(&self.git_repositories); self.git_repositories .iter() .rev() //git_repository is ordered lexicographically @@ -1725,7 +1702,6 @@ impl LocalSnapshot { impl GitRepositoryEntry { // Note that these paths should be relative to the worktree root. pub(crate) fn manages(&self, path: &Path) -> bool { - dbg!(path, &self.content_path); path.starts_with(self.content_path.as_ref()) } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 3bf09436edd81ed5bf83affd0a81ab0ff9ff359c..fd04fc0aa66ad7741b598aa84f5c9d66e7d45800 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -57,34 +57,19 @@ impl FeatureFlags { #[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, + pub gutter_debounce: Option, } #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] -pub struct GitGutter { - pub files_included: Option, - pub debounce_delay_millis: Option, -} - -impl GitGutter { - pub fn files_included(&self, settings: &Settings) -> GitFilesIncluded { - self.files_included.unwrap_or_else(|| { - settings - .git.git_gutter.expect("git_gutter must be some in defaults.json") - .files_included - .expect("Should be some in defaults.json") - }) - } -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] -pub enum GitFilesIncluded { +pub enum GitGutter { #[default] - All, - OnlyTracked, - None, + TrackedFiles, + Hide, } +pub struct GitGutterConfig {} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] pub struct EditorSettings { pub tab_size: Option, @@ -428,12 +413,7 @@ impl Settings { editor_overrides: Default::default(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), - git: GitSettings { - git_gutter: Some(GitGutter { - files_included: Some(GitFilesIncluded::All), - debounce_delay_millis: None, - }), - }, + git: Default::default(), git_overrides: Default::default(), language_defaults: Default::default(), language_overrides: Default::default(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44c9b19f1bbd419b6e6eceb3b3c39c31fc587bec..2ae498d7015053652497bcfce07b11ae750117fd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -735,16 +735,7 @@ impl ItemHandle for ViewHandle { } let settings = cx.global::(); - let debounce_delay = settings - .git_overrides - .git_gutter - .unwrap_or_else(|| { - settings - .git - .git_gutter - .expect("This should be Some by setting setup") - }) - .debounce_delay_millis; + let debounce_delay = settings.git_overrides.gutter_debounce; let item = item.clone(); From 2bd947d4d07c7b5679c986301e727174d4d35491 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 4 Oct 2022 14:58:44 -0400 Subject: [PATCH 73/73] Use correct start row for hunk retrieval & correct paint offset Co-Authored-By: Joseph Lyons --- crates/editor/src/element.rs | 63 +++++++++++++----------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 56887f4b45600a8b5c50feb97bff3f94170b2cfb..2b93255972c41eaa446f01fa77c8ed57a26279bb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -534,23 +534,22 @@ impl EditorElement { } struct DiffLayout<'a> { - buffer_line: usize, - last_diff: Option<(&'a DiffHunk, usize)>, + buffer_row: u32, + last_diff: Option<&'a DiffHunk>, } fn diff_quad( - status: DiffHunkStatus, - layout_range: Range, + hunk: &DiffHunk, gutter_layout: &GutterLayout, diff_style: &DiffStyle, ) -> Quad { - let color = match status { + let color = match hunk.status() { DiffHunkStatus::Added => diff_style.inserted, DiffHunkStatus::Modified => diff_style.modified, //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { - let row = layout_range.start; + let row = hunk.buffer_range.start; let offset = gutter_layout.line_height / 2.; let start_y = @@ -571,8 +570,8 @@ impl EditorElement { } }; - let start_row = layout_range.start; - let end_row = layout_range.end; + let start_row = hunk.buffer_range.start; + let end_row = hunk.buffer_range.end; let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top; let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top; @@ -590,19 +589,18 @@ impl EditorElement { } } + let scroll_position = layout.position_map.snapshot.scroll_position(); let gutter_layout = { - let scroll_position = layout.position_map.snapshot.scroll_position(); let line_height = layout.position_map.line_height; GutterLayout { scroll_top: scroll_position.y() * line_height, - // scroll_position, line_height, bounds, } }; let mut diff_layout = DiffLayout { - buffer_line: 0, + buffer_row: scroll_position.y() as u32, last_diff: None, }; @@ -629,49 +627,35 @@ impl EditorElement { if show_gutter { //This line starts a buffer line, so let's do the diff calculation - let new_hunk = get_hunk(diff_layout.buffer_line, &layout.diff_hunks); + let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks); - // This + the unwraps are annoying, but at least it's legible let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) { - (None, None) => (false, false), - (None, Some(_)) => (false, true), - (Some(_), None) => (true, false), - (Some((old_hunk, _)), Some(new_hunk)) if new_hunk == old_hunk => { + (Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => { (false, false) } - (Some(_), Some(_)) => (true, true), + (a, b) => (a.is_some(), b.is_some()), }; if is_ending { - let (last_hunk, start_line) = diff_layout.last_diff.take().unwrap(); - cx.scene.push_quad(diff_quad( - last_hunk.status(), - start_line..ix, - &gutter_layout, - diff_style, - )); + let last_hunk = diff_layout.last_diff.take().unwrap(); + cx.scene + .push_quad(diff_quad(last_hunk, &gutter_layout, diff_style)); } if is_starting { let new_hunk = new_hunk.unwrap(); - - diff_layout.last_diff = Some((new_hunk, ix)); + diff_layout.last_diff = Some(new_hunk); }; - diff_layout.buffer_line += 1; + diff_layout.buffer_row += 1; } } } - // If we ran out with a diff hunk still being prepped, paint it now - if let Some((last_hunk, start_line)) = diff_layout.last_diff { - let end_line = layout.line_number_layouts.len(); - cx.scene.push_quad(diff_quad( - last_hunk.status(), - start_line..end_line, - &gutter_layout, - diff_style, - )) + // If we ran out with a diff hunk still being prepped, paint it now + if let Some(last_hunk) = diff_layout.last_diff { + cx.scene + .push_quad(diff_quad(last_hunk, &gutter_layout, diff_style)) } if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { @@ -1385,14 +1369,13 @@ impl EditorElement { /// Get the hunk that contains buffer_line, starting from start_idx /// Returns none if there is none found, and -fn get_hunk(buffer_line: usize, hunks: &[DiffHunk]) -> Option<&DiffHunk> { +fn get_hunk(buffer_line: u32, hunks: &[DiffHunk]) -> Option<&DiffHunk> { for i in 0..hunks.len() { // Safety: Index out of bounds is handled by the check above let hunk = hunks.get(i).unwrap(); if hunk.buffer_range.contains(&(buffer_line as u32)) { return Some(hunk); - } else if hunk.status() == DiffHunkStatus::Removed - && buffer_line == hunk.buffer_range.start as usize + } else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start { return Some(hunk); } else if hunk.buffer_range.start > buffer_line as u32 {