Add debug methods for visually annotating ranges (#38097)
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
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user