Avoid racing git diffs & allow for "as fast as possible" diff updating

Julia and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>

Change summary

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(-)

Detailed changes

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: 

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<Arc<String>>,
-    git_diff: git::BufferDiff,
+    git_diff_status: GitDiffStatus,
     file: Option<Arc<dyn File>>,
     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<Arc<dyn File>>,
     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<Self>) {
+        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<u32>,
     ) -> impl 'a + Iterator<Item = git::DiffHunk<u32>> {
-        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(),

crates/settings/src/settings.rs 🔗

@@ -61,6 +61,22 @@ pub struct EditorSettings {
     pub format_on_save: Option<FormatOnSave>,
     pub formatter: Option<Formatter>,
     pub enable_language_server: Option<bool>,
+    pub git_gutter: Option<GitGutterConfig>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
+pub struct GitGutterConfig {
+    pub files_included: GitGutterLevel,
+    pub debounce_delay_millis: Option<u64>,
+}
+
+#[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(),

crates/workspace/src/workspace.rs 🔗

@@ -734,18 +734,41 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     );
                                 }
 
-                                const GIT_DELAY: Duration = Duration::from_millis(10);
+                                let debounce_delay = cx
+                                    .global::<Settings>()
+                                    .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();
+                                }
                             }
 
                             _ => {}