Enhance keyboard navigation when showing next diagnostic

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/buffer/src/anchor.rs      |   6 +
crates/editor/src/display_map.rs |   5 
crates/editor/src/lib.rs         | 112 +++++++++++++++++++++++++++------
crates/language/src/lib.rs       |   4 
4 files changed, 99 insertions(+), 28 deletions(-)

Detailed changes

crates/buffer/src/anchor.rs 🔗

@@ -527,6 +527,7 @@ impl<'a> sum_tree::SeekTarget<'a, AnchorRangeMultimapSummary, FullOffsetRange> f
 
 pub trait AnchorRangeExt {
     fn cmp<'a>(&self, b: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
+    fn to_offset<'a>(&self, content: impl Into<Content<'a>>) -> Range<usize>;
 }
 
 impl AnchorRangeExt for Range<Anchor> {
@@ -537,4 +538,9 @@ impl AnchorRangeExt for Range<Anchor> {
             ord @ _ => ord,
         })
     }
+
+    fn to_offset<'a>(&self, content: impl Into<Content<'a>>) -> Range<usize> {
+        let content = content.into();
+        self.start.to_offset(&content)..self.end.to_offset(&content)
+    }
 }

crates/editor/src/display_map.rs 🔗

@@ -4,7 +4,8 @@ mod patch;
 mod tab_map;
 mod wrap_map;
 
-use block_map::{BlockId, BlockMap, BlockPoint};
+pub use block_map::{BlockDisposition, BlockId, BlockProperties, BufferRows, Chunks};
+use block_map::{BlockMap, BlockPoint};
 use buffer::Rope;
 use fold_map::{FoldMap, ToFoldPoint as _};
 use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
@@ -15,8 +16,6 @@ use tab_map::TabMap;
 use theme::SyntaxTheme;
 use wrap_map::WrapMap;
 
-pub use block_map::{BlockDisposition, BlockProperties, BufferRows, Chunks};
-
 pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint;
 }

crates/editor/src/lib.rs 🔗

@@ -24,6 +24,7 @@ use smol::Timer;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
+    collections::HashSet,
     iter, mem,
     ops::{Range, RangeInclusive},
     rc::Rc,
@@ -304,6 +305,7 @@ pub struct Editor {
     add_selections_state: Option<AddSelectionsState>,
     autoclose_stack: Vec<BracketPairState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
+    active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
     autoscroll_requested: bool,
@@ -336,6 +338,12 @@ struct BracketPairState {
     pair: BracketPair,
 }
 
+#[derive(Debug)]
+struct ActiveDiagnosticGroup {
+    primary_range: Range<Anchor>,
+    block_ids: HashSet<BlockId>,
+}
+
 #[derive(Serialize, Deserialize)]
 struct ClipboardSelection {
     len: usize,
@@ -423,6 +431,7 @@ impl Editor {
             add_selections_state: None,
             autoclose_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
+            active_diagnostics: None,
             build_settings,
             scroll_position: Vector2F::zero(),
             scroll_top_anchor: Anchor::min(),
@@ -2205,36 +2214,93 @@ impl Editor {
     }
 
     pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext<Self>) {
-        let selection = self.newest_selection(cx);
+        let selection = self.newest_selection::<usize>(cx);
         let buffer = self.buffer.read(cx.as_ref());
-        let diagnostic_group_id = dbg!(buffer
-            .diagnostics_in_range::<_, usize>(selection.head()..buffer.len())
-            .filter(|(_, diagnostic)| diagnostic.is_primary)
-            .next())
-        .map(|(_, diagnostic)| diagnostic.group_id);
+        let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
+            active_diagnostics
+                .primary_range
+                .to_offset(buffer)
+                .to_inclusive()
+        });
+        let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
+            if active_primary_range.contains(&selection.head()) {
+                *active_primary_range.end()
+            } else {
+                selection.head()
+            }
+        } else {
+            selection.head()
+        };
 
-        if let Some(group_id) = diagnostic_group_id {
-            self.display_map.update(cx, |display_map, cx| {
-                let buffer = self.buffer.read(cx);
-                let diagnostic_group = buffer
-                    .diagnostic_group::<Point>(group_id)
-                    .map(|(range, diagnostic)| (range, diagnostic.message.clone()))
-                    .collect::<Vec<_>>();
+        loop {
+            let next_group = buffer
+                .diagnostics_in_range::<_, usize>(search_start..buffer.len())
+                .filter(|(_, diagnostic)| diagnostic.is_primary)
+                .skip_while(|(range, _)| {
+                    Some(range.end) == active_primary_range.as_ref().map(|r| *r.end())
+                })
+                .next()
+                .map(|(range, diagnostic)| (range, diagnostic.group_id));
+
+            if let Some((primary_range, group_id)) = next_group {
+                self.dismiss_diagnostics(cx);
+                self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
+                    let buffer = self.buffer.read(cx);
+                    let diagnostic_group = buffer
+                        .diagnostic_group::<Point>(group_id)
+                        .map(|(range, diagnostic)| (range, diagnostic.message.clone()))
+                        .collect::<Vec<_>>();
+                    let primary_range = buffer.anchor_after(primary_range.start)
+                        ..buffer.anchor_before(primary_range.end);
+
+                    let block_ids = display_map
+                        .insert_blocks(
+                            diagnostic_group
+                                .iter()
+                                .map(|(range, message)| BlockProperties {
+                                    position: range.start,
+                                    text: message.as_str(),
+                                    runs: vec![],
+                                    disposition: BlockDisposition::Above,
+                                }),
+                            cx,
+                        )
+                        .into_iter()
+                        .collect();
 
-                dbg!(group_id, &diagnostic_group);
+                    Some(ActiveDiagnosticGroup {
+                        primary_range,
+                        block_ids,
+                    })
+                });
 
-                display_map.insert_blocks(
-                    diagnostic_group
-                        .iter()
-                        .map(|(range, message)| BlockProperties {
-                            position: range.start,
-                            text: message.as_str(),
-                            runs: vec![],
-                            disposition: BlockDisposition::Above,
-                        }),
+                self.update_selections(
+                    vec![Selection {
+                        id: selection.id,
+                        start: primary_range.start,
+                        end: primary_range.start,
+                        reversed: false,
+                        goal: SelectionGoal::None,
+                    }],
+                    true,
                     cx,
                 );
+                break;
+            } else if search_start == 0 {
+                break;
+            } else {
+                // Cycle around to the start of the buffer.
+                search_start = 0;
+            }
+        }
+    }
+
+    fn dismiss_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
+            self.display_map.update(cx, |display_map, cx| {
+                display_map.remove_blocks(active_diagnostic_group.block_ids, cx);
             });
+            cx.notify();
         }
     }
 

crates/language/src/lib.rs 🔗

@@ -816,7 +816,7 @@ impl Buffer {
 
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,
-        range: Range<T>,
+        search_range: Range<T>,
     ) -> impl Iterator<Item = (Range<O>, &Diagnostic)> + 'a
     where
         T: 'a + ToOffset,
@@ -824,7 +824,7 @@ impl Buffer {
     {
         let content = self.content();
         self.diagnostics
-            .intersecting_ranges(range, content, true)
+            .intersecting_ranges(search_range, content, true)
             .map(move |(_, range, diagnostic)| (range, diagnostic))
     }