Add debug methods for visually annotating ranges (#38097)

Michael Sloan created

This allows you to write `buffer_snapshot.debug(ranges, value)` and it
will be displayed in the buffer (or multibuffer!) until that callsite
runs again. `ranges` can be any position (`usize`, `Anchor`, etc), any
range, or a slice or vec of those. `value` just needs a `Debug` impl.
These are stored in a mutable global for convenience, and this is only
available in debug builds.

For example, using this to visualize the captures of the brackets
Tree-sitter query:

<img width="1215" height="480" alt="image"
src="https://github.com/user-attachments/assets/c1878fc7-f6b3-4e27-949e-ecf67a7906b9"
/>

Release Notes:

- N/A

Change summary

crates/editor/src/element.rs            |  57 ++++++++
crates/multi_buffer/src/multi_buffer.rs | 111 +++++++++++++++
crates/text/src/text.rs                 | 189 +++++++++++++++++++++++++++
3 files changed, 357 insertions(+)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1505,6 +1505,15 @@ impl EditorElement {
                 selections.push((player, layouts));
             }
         });
+
+        #[cfg(debug_assertions)]
+        Self::layout_debug_ranges(
+            &mut selections,
+            start_anchor..end_anchor,
+            &snapshot.display_snapshot,
+            cx,
+        );
+
         (selections, active_rows, newest_selection_head)
     }
 
@@ -7294,6 +7303,54 @@ impl EditorElement {
 
         unstaged == unstaged_hollow
     }
+
+    #[cfg(debug_assertions)]
+    fn layout_debug_ranges(
+        selections: &mut Vec<(PlayerColor, Vec<SelectionLayout>)>,
+        anchor_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        cx: &App,
+    ) {
+        let theme = cx.theme();
+        text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
+            if debug_ranges.ranges.is_empty() {
+                return;
+            }
+            let buffer_snapshot = &display_snapshot.buffer_snapshot;
+            for (buffer, buffer_range, excerpt_id) in
+                buffer_snapshot.range_to_buffer_ranges(anchor_range)
+            {
+                let buffer_range =
+                    buffer.anchor_after(buffer_range.start)..buffer.anchor_before(buffer_range.end);
+                selections.extend(debug_ranges.ranges.iter().flat_map(|debug_range| {
+                    let player_color = theme
+                        .players()
+                        .color_for_participant(debug_range.occurrence_index as u32 + 1);
+                    debug_range.ranges.iter().filter_map(move |range| {
+                        if range.start.buffer_id != Some(buffer.remote_id()) {
+                            return None;
+                        }
+                        let clipped_start = range.start.max(&buffer_range.start, buffer);
+                        let clipped_end = range.end.min(&buffer_range.end, buffer);
+                        let range = buffer_snapshot.anchor_in_excerpt(excerpt_id, clipped_start)?
+                            ..buffer_snapshot.anchor_in_excerpt(excerpt_id, clipped_end)?;
+                        let start = range.start.to_display_point(display_snapshot);
+                        let end = range.end.to_display_point(display_snapshot);
+                        let selection_layout = SelectionLayout {
+                            head: start,
+                            range: start..end,
+                            cursor_shape: CursorShape::Bar,
+                            is_newest: false,
+                            is_local: false,
+                            active_rows: start.row()..end.row(),
+                            user_name: Some(SharedString::new(debug_range.value.clone())),
+                        };
+                        Some((player_color, vec![selection_layout]))
+                    })
+                }));
+            }
+        });
+    }
 }
 
 fn header_jump_data(

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -6402,6 +6402,45 @@ impl MultiBufferSnapshot {
     pub fn diff_for_buffer_id(&self, buffer_id: BufferId) -> Option<&BufferDiffSnapshot> {
         self.diffs.get(&buffer_id)
     }
+
+    /// Visually annotates a position or range with the `Debug` representation of a value. The
+    /// callsite of this function is used as a key - previous annotations will be removed.
+    #[cfg(debug_assertions)]
+    #[track_caller]
+    pub fn debug<V, R>(&self, ranges: &R, value: V)
+    where
+        R: debug::ToMultiBufferDebugRanges,
+        V: std::fmt::Debug,
+    {
+        self.debug_with_key(std::panic::Location::caller(), ranges, value);
+    }
+
+    /// Visually annotates a position or range with the `Debug` representation of a value. Previous
+    /// debug annotations with the same key will be removed. The key is also used to determine the
+    /// annotation's color.
+    #[cfg(debug_assertions)]
+    #[track_caller]
+    pub fn debug_with_key<K, R, V>(&self, key: &K, ranges: &R, value: V)
+    where
+        K: std::hash::Hash + 'static,
+        R: debug::ToMultiBufferDebugRanges,
+        V: std::fmt::Debug,
+    {
+        let text_ranges = ranges
+            .to_multi_buffer_debug_ranges(self)
+            .into_iter()
+            .flat_map(|range| {
+                self.range_to_buffer_ranges(range).into_iter().map(
+                    |(buffer, range, _excerpt_id)| {
+                        buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
+                    },
+                )
+            })
+            .collect();
+        text::debug::GlobalDebugRanges::with_locked(|debug_ranges| {
+            debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
+        });
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -7983,3 +8022,75 @@ impl From<ExcerptId> for EntityId {
         EntityId::from(id.0 as u64)
     }
 }
+
+#[cfg(debug_assertions)]
+pub mod debug {
+    use super::*;
+
+    pub trait ToMultiBufferDebugRanges {
+        fn to_multi_buffer_debug_ranges(&self, snapshot: &MultiBufferSnapshot)
+        -> Vec<Range<usize>>;
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for T {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            [self.to_offset(snapshot)].to_multi_buffer_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for Range<T> {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            [self.start.to_offset(snapshot)..self.end.to_offset(snapshot)]
+                .to_multi_buffer_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for Vec<T> {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            self.as_slice().to_multi_buffer_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for Vec<Range<T>> {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            self.as_slice().to_multi_buffer_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for [T] {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            self.iter()
+                .map(|item| {
+                    let offset = item.to_offset(snapshot);
+                    offset..offset
+                })
+                .collect()
+        }
+    }
+
+    impl<T: ToOffset> ToMultiBufferDebugRanges for [Range<T>] {
+        fn to_multi_buffer_debug_ranges(
+            &self,
+            snapshot: &MultiBufferSnapshot,
+        ) -> Vec<Range<usize>> {
+            self.iter()
+                .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot))
+                .collect()
+        }
+    }
+}

crates/text/src/text.rs 🔗

@@ -2593,6 +2593,38 @@ impl BufferSnapshot {
             last_old_end + new_offset.saturating_sub(last_new_end)
         })
     }
+
+    /// Visually annotates a position or range with the `Debug` representation of a value. The
+    /// callsite of this function is used as a key - previous annotations will be removed.
+    #[cfg(debug_assertions)]
+    #[track_caller]
+    pub fn debug<R, V>(&self, ranges: &R, value: V)
+    where
+        R: debug::ToDebugRanges,
+        V: std::fmt::Debug,
+    {
+        self.debug_with_key(std::panic::Location::caller(), ranges, value);
+    }
+
+    /// Visually annotates a position or range with the `Debug` representation of a value. Previous
+    /// debug annotations with the same key will be removed. The key is also used to determine the
+    /// annotation's color.
+    #[cfg(debug_assertions)]
+    pub fn debug_with_key<K, R, V>(&self, key: &K, ranges: &R, value: V)
+    where
+        K: std::hash::Hash + 'static,
+        R: debug::ToDebugRanges,
+        V: std::fmt::Debug,
+    {
+        let ranges = ranges
+            .to_debug_ranges(self)
+            .into_iter()
+            .map(|range| self.anchor_after(range.start)..self.anchor_before(range.end))
+            .collect();
+        debug::GlobalDebugRanges::with_locked(|debug_ranges| {
+            debug_ranges.insert(key, ranges, format!("{value:?}").into());
+        });
+    }
 }
 
 struct RopeBuilder<'a> {
@@ -3247,3 +3279,160 @@ impl LineEnding {
         }
     }
 }
+
+#[cfg(debug_assertions)]
+pub mod debug {
+    use super::*;
+    use parking_lot::Mutex;
+    use std::any::TypeId;
+    use std::hash::{Hash, Hasher};
+
+    static GLOBAL_DEBUG_RANGES: Mutex<Option<GlobalDebugRanges>> = Mutex::new(None);
+
+    pub struct GlobalDebugRanges {
+        pub ranges: Vec<DebugRange>,
+        key_to_occurrence_index: HashMap<Key, usize>,
+        next_occurrence_index: usize,
+    }
+
+    pub struct DebugRange {
+        key: Key,
+        pub ranges: Vec<Range<Anchor>>,
+        pub value: Arc<str>,
+        pub occurrence_index: usize,
+    }
+
+    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
+    struct Key {
+        type_id: TypeId,
+        hash: u64,
+    }
+
+    impl GlobalDebugRanges {
+        pub fn with_locked<R>(f: impl FnOnce(&mut Self) -> R) -> R {
+            let mut state = GLOBAL_DEBUG_RANGES.lock();
+            if state.is_none() {
+                *state = Some(GlobalDebugRanges {
+                    ranges: Vec::new(),
+                    key_to_occurrence_index: HashMap::default(),
+                    next_occurrence_index: 0,
+                });
+            }
+            if let Some(global_debug_ranges) = state.as_mut() {
+                f(global_debug_ranges)
+            } else {
+                unreachable!()
+            }
+        }
+
+        pub fn insert<K: Hash + 'static>(
+            &mut self,
+            key: &K,
+            ranges: Vec<Range<Anchor>>,
+            value: Arc<str>,
+        ) {
+            let occurrence_index = *self
+                .key_to_occurrence_index
+                .entry(Key::new(key))
+                .or_insert_with(|| {
+                    let occurrence_index = self.next_occurrence_index;
+                    self.next_occurrence_index += 1;
+                    occurrence_index
+                });
+            let key = Key::new(key);
+            let existing = self
+                .ranges
+                .iter()
+                .enumerate()
+                .rfind(|(_, existing)| existing.key == key);
+            if let Some((existing_ix, _)) = existing {
+                self.ranges.remove(existing_ix);
+            }
+            self.ranges.push(DebugRange {
+                ranges,
+                key,
+                value,
+                occurrence_index,
+            });
+        }
+
+        pub fn remove<K: Hash + 'static>(&mut self, key: &K) {
+            self.remove_impl(&Key::new(key));
+        }
+
+        fn remove_impl(&mut self, key: &Key) {
+            let existing = self
+                .ranges
+                .iter()
+                .enumerate()
+                .rfind(|(_, existing)| &existing.key == key);
+            if let Some((existing_ix, _)) = existing {
+                self.ranges.remove(existing_ix);
+            }
+        }
+
+        pub fn remove_all_with_key_type<K: 'static>(&mut self) {
+            self.ranges
+                .retain(|item| item.key.type_id != TypeId::of::<K>());
+        }
+    }
+
+    impl Key {
+        fn new<K: Hash + 'static>(key: &K) -> Self {
+            let type_id = TypeId::of::<K>();
+            let mut hasher = collections::FxHasher::default();
+            key.hash(&mut hasher);
+            Key {
+                type_id,
+                hash: hasher.finish(),
+            }
+        }
+    }
+
+    pub trait ToDebugRanges {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>>;
+    }
+
+    impl<T: ToOffset> ToDebugRanges for T {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            [self.to_offset(snapshot)].to_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset + Clone> ToDebugRanges for Range<T> {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            [self.clone()].to_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToDebugRanges for Vec<T> {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            self.as_slice().to_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToDebugRanges for Vec<Range<T>> {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            self.as_slice().to_debug_ranges(snapshot)
+        }
+    }
+
+    impl<T: ToOffset> ToDebugRanges for [T] {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            self.iter()
+                .map(|item| {
+                    let offset = item.to_offset(snapshot);
+                    offset..offset
+                })
+                .collect()
+        }
+    }
+
+    impl<T: ToOffset> ToDebugRanges for [Range<T>] {
+        fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec<Range<usize>> {
+            self.iter()
+                .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot))
+                .collect()
+        }
+    }
+}