Use MultiBuffer::insert_excerpt_after to update project diagnostics view

Max Brunsfeld created

Change summary

crates/diagnostics/src/diagnostics.rs | 155 +++++++++++++++++++++++++---
1 file changed, 138 insertions(+), 17 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,8 +1,9 @@
 use anyhow::Result;
+use collections::{hash_map, HashMap, HashSet};
 use editor::{
     context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
-    display_map::{BlockDisposition, BlockProperties},
-    BuildSettings, Editor, ExcerptProperties, MultiBuffer,
+    display_map::{BlockDisposition, BlockId, BlockProperties},
+    BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer,
 };
 use gpui::{
     action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
@@ -11,7 +12,7 @@ use gpui::{
 use language::{Bias, Buffer, Point};
 use postage::watch;
 use project::Project;
-use std::ops::Range;
+use std::{ops::Range, path::Path, sync::Arc};
 use util::TryFutureExt;
 use workspace::Workspace;
 
@@ -33,9 +34,22 @@ struct ProjectDiagnostics {
 struct ProjectDiagnosticsEditor {
     editor: ViewHandle<Editor>,
     excerpts: ModelHandle<MultiBuffer>,
+    path_states: Vec<(Arc<Path>, PathState)>,
     build_settings: BuildSettings,
 }
 
+#[derive(Default)]
+struct PathState {
+    last_excerpt: ExcerptId,
+    diagnostic_group_states: HashMap<usize, DiagnosticGroupState>,
+}
+
+#[derive(Default)]
+struct DiagnosticGroupState {
+    excerpts: Vec<ExcerptId>,
+    blocks: Vec<BlockId>,
+}
+
 impl ProjectDiagnostics {
     fn new(project: ModelHandle<Project>) -> Self {
         Self { project }
@@ -118,6 +132,7 @@ impl ProjectDiagnosticsEditor {
             excerpts,
             editor,
             build_settings,
+            path_states: Default::default(),
         }
     }
 
@@ -132,13 +147,66 @@ impl ProjectDiagnosticsEditor {
     }
 
     fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
-        let mut blocks = Vec::new();
-        let snapshot = buffer.read(cx).snapshot();
+        let snapshot;
+        let path;
+        {
+            let buffer = buffer.read(cx);
+            snapshot = buffer.snapshot();
+            if let Some(file) = buffer.file() {
+                path = file.path().clone();
+            } else {
+                return;
+            }
+        }
 
+        let path_ix = match self
+            .path_states
+            .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
+        {
+            Ok(ix) => ix,
+            Err(ix) => {
+                self.path_states.insert(
+                    ix,
+                    (
+                        path.clone(),
+                        PathState {
+                            last_excerpt: ExcerptId::max(),
+                            diagnostic_group_states: Default::default(),
+                        },
+                    ),
+                );
+                ix
+            }
+        };
+        let mut prev_excerpt_id = if path_ix > 0 {
+            self.path_states[path_ix - 1].1.last_excerpt.clone()
+        } else {
+            ExcerptId::min()
+        };
+        let path_state = &mut self.path_states[path_ix].1;
+
+        let mut blocks_to_add = Vec::new();
+        let mut blocks_to_remove = HashSet::default();
+        let mut excerpts_to_remove = Vec::new();
+        let mut block_counts_by_group = Vec::new();
+
+        let diagnostic_groups = snapshot.diagnostic_groups::<Point>();
         let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
-            for group in snapshot.diagnostic_groups::<Point>() {
+            for group in &diagnostic_groups {
+                let group_id = group.entries[0].diagnostic.group_id;
+
+                let group_state = match path_state.diagnostic_group_states.entry(group_id) {
+                    hash_map::Entry::Occupied(e) => {
+                        prev_excerpt_id = e.get().excerpts.last().unwrap().clone();
+                        block_counts_by_group.push(0);
+                        continue;
+                    }
+                    hash_map::Entry::Vacant(e) => e.insert(DiagnosticGroupState::default()),
+                };
+
+                let mut block_count = 0;
                 let mut pending_range: Option<(Range<Point>, usize)> = None;
-                let mut is_first_excerpt = true;
+                let mut is_first_excerpt_for_group = true;
                 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
                     if let Some((range, start_ix)) = &mut pending_range {
                         if let Some(entry) = entry {
@@ -154,7 +222,8 @@ impl ProjectDiagnosticsEditor {
                             Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
                             Bias::Left,
                         );
-                        let excerpt_id = excerpts.push_excerpt(
+                        let excerpt_id = excerpts.insert_excerpt_after(
+                            &prev_excerpt_id,
                             ExcerptProperties {
                                 buffer: &buffer,
                                 range: excerpt_start..excerpt_end,
@@ -162,13 +231,18 @@ impl ProjectDiagnosticsEditor {
                             excerpts_cx,
                         );
 
+                        prev_excerpt_id = excerpt_id.clone();
+                        group_state.excerpts.push(excerpt_id.clone());
                         let header_position = (excerpt_id.clone(), language::Anchor::min());
-                        if is_first_excerpt {
+
+                        if is_first_excerpt_for_group {
+                            is_first_excerpt_for_group = false;
                             let primary = &group.entries[group.primary_ix].diagnostic;
                             let mut header = primary.clone();
                             header.message =
                                 primary.message.split('\n').next().unwrap().to_string();
-                            blocks.push(BlockProperties {
+                            block_count += 1;
+                            blocks_to_add.push(BlockProperties {
                                 position: header_position,
                                 height: 2,
                                 render: diagnostic_header_renderer(
@@ -179,7 +253,8 @@ impl ProjectDiagnosticsEditor {
                                 disposition: BlockDisposition::Above,
                             });
                         } else {
-                            blocks.push(BlockProperties {
+                            block_count += 1;
+                            blocks_to_add.push(BlockProperties {
                                 position: header_position,
                                 height: 1,
                                 render: context_header_renderer(self.build_settings.clone()),
@@ -187,7 +262,6 @@ impl ProjectDiagnosticsEditor {
                             });
                         }
 
-                        is_first_excerpt = false;
                         for entry in &group.entries[*start_ix..ix] {
                             let mut diagnostic = entry.diagnostic.clone();
                             if diagnostic.is_primary {
@@ -198,7 +272,8 @@ impl ProjectDiagnosticsEditor {
 
                             if !diagnostic.message.is_empty() {
                                 let buffer_anchor = snapshot.anchor_before(entry.range.start);
-                                blocks.push(BlockProperties {
+                                block_count += 1;
+                                blocks_to_add.push(BlockProperties {
                                     position: (excerpt_id.clone(), buffer_anchor),
                                     height: diagnostic.message.matches('\n').count() as u8 + 1,
                                     render: diagnostic_block_renderer(
@@ -218,14 +293,36 @@ impl ProjectDiagnosticsEditor {
                         pending_range = Some((entry.range.clone(), ix));
                     }
                 }
+
+                block_counts_by_group.push(block_count);
             }
 
+            path_state
+                .diagnostic_group_states
+                .retain(|group_id, group_state| {
+                    if diagnostic_groups
+                        .iter()
+                        .any(|group| group.entries[0].diagnostic.group_id == *group_id)
+                    {
+                        true
+                    } else {
+                        excerpts_to_remove.extend(group_state.excerpts.drain(..));
+                        blocks_to_remove.extend(group_state.blocks.drain(..));
+                        false
+                    }
+                });
+
+            excerpts_to_remove.sort();
+            excerpts.remove_excerpts(excerpts_to_remove.iter(), excerpts_cx);
             excerpts.snapshot(excerpts_cx)
         });
 
+        path_state.last_excerpt = prev_excerpt_id;
+
         self.editor.update(cx, |editor, cx| {
-            editor.insert_blocks(
-                blocks.into_iter().map(|block| {
+            editor.remove_blocks(blocks_to_remove, cx);
+            let block_ids = editor.insert_blocks(
+                blocks_to_add.into_iter().map(|block| {
                     let (excerpt_id, text_anchor) = block.position;
                     BlockProperties {
                         position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
@@ -236,6 +333,20 @@ impl ProjectDiagnosticsEditor {
                 }),
                 cx,
             );
+
+            let mut block_ids = block_ids.into_iter();
+            let mut block_counts_by_group = block_counts_by_group.into_iter();
+            for group in &diagnostic_groups {
+                let group_id = group.entries[0].diagnostic.group_id;
+                let block_count = block_counts_by_group.next().unwrap();
+                let group_state = path_state
+                    .diagnostic_group_states
+                    .get_mut(&group_id)
+                    .unwrap();
+                group_state
+                    .blocks
+                    .extend(block_ids.by_ref().take(block_count));
+            }
         });
         cx.notify();
     }
@@ -454,7 +565,9 @@ mod tests {
             assert_eq!(
                 editor.text(),
                 concat!(
-                    // Diagnostic group 1 (error for `y`)
+                    //
+                    // main.rs, diagnostic group 1
+                    //
                     "\n", // primary message
                     "\n", // filename
                     "    let x = vec![];\n",
@@ -468,7 +581,9 @@ mod tests {
                     "    c(y);\n",
                     "\n", // supporting diagnostic
                     "    d(x);\n",
-                    // Diagnostic group 2 (error for `x`)
+                    //
+                    // main.rs, diagnostic group 2
+                    //
                     "\n", // primary message
                     "\n", // filename
                     "fn main() {\n",
@@ -516,11 +631,15 @@ mod tests {
             assert_eq!(
                 editor.text(),
                 concat!(
+                    //
                     // a.rs
+                    //
                     "\n", // primary message
                     "\n", // filename
                     "const a: i32 = 'a';\n",
+                    //
                     // main.rs, diagnostic group 1
+                    //
                     "\n", // primary message
                     "\n", // filename
                     "    let x = vec![];\n",
@@ -534,7 +653,9 @@ mod tests {
                     "    c(y);\n",
                     "\n", // supporting diagnostic
                     "    d(x);\n",
+                    //
                     // main.rs, diagnostic group 2
+                    //
                     "\n", // primary message
                     "\n", // filename
                     "fn main() {\n",