Unnamed repository; edit this file 'description' to name the repository.
internal: Improve reporting of intersecting changes
Giga Bowser 2025-02-25
parent 8925544 · commit b4f2d62
-rw-r--r--crates/syntax/src/syntax_editor.rs59
-rw-r--r--crates/syntax/src/syntax_editor/edit_algo.rs87
-rw-r--r--crates/test-utils/src/lib.rs4
3 files changed, 142 insertions, 8 deletions
diff --git a/crates/syntax/src/syntax_editor.rs b/crates/syntax/src/syntax_editor.rs
index b82181ae13..48c160b9a9 100644
--- a/crates/syntax/src/syntax_editor.rs
+++ b/crates/syntax/src/syntax_editor.rs
@@ -5,6 +5,7 @@
//! [`SyntaxEditor`]: https://github.com/dotnet/roslyn/blob/43b0b05cc4f492fd5de00f6f6717409091df8daa/src/Workspaces/Core/Portable/Editing/SyntaxEditor.cs
use std::{
+ fmt,
num::NonZeroU32,
ops::RangeInclusive,
sync::atomic::{AtomicU32, Ordering},
@@ -282,6 +283,64 @@ enum ChangeKind {
Replace,
}
+impl fmt::Display for Change {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Change::Insert(position, node_or_token) => {
+ let parent = position.parent();
+ let mut parent_str = parent.to_string();
+ let target_range = self.target_range().start() - parent.text_range().start();
+
+ parent_str.insert_str(
+ target_range.into(),
+ &format!("\x1b[42m{node_or_token}\x1b[0m\x1b[K"),
+ );
+ f.write_str(&parent_str)
+ }
+ Change::InsertAll(position, vec) => {
+ let parent = position.parent();
+ let mut parent_str = parent.to_string();
+ let target_range = self.target_range().start() - parent.text_range().start();
+ let insertion: String = vec.iter().map(|it| it.to_string()).collect();
+
+ parent_str
+ .insert_str(target_range.into(), &format!("\x1b[42m{insertion}\x1b[0m\x1b[K"));
+ f.write_str(&parent_str)
+ }
+ Change::Replace(old, new) => {
+ if let Some(new) = new {
+ write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
+ } else {
+ write!(f, "\x1b[41m{old}\x1b[0m\x1b[K")
+ }
+ }
+ Change::ReplaceWithMany(old, vec) => {
+ let new: String = vec.iter().map(|it| it.to_string()).collect();
+ write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
+ }
+ Change::ReplaceAll(range, vec) => {
+ let parent = range.start().parent().unwrap();
+ let parent_str = parent.to_string();
+ let pre_range =
+ TextRange::new(parent.text_range().start(), range.start().text_range().start());
+ let old_range = TextRange::new(
+ range.start().text_range().start(),
+ range.end().text_range().end(),
+ );
+ let post_range =
+ TextRange::new(range.end().text_range().end(), parent.text_range().end());
+
+ let pre_str = &parent_str[pre_range - parent.text_range().start()];
+ let old_str = &parent_str[old_range - parent.text_range().start()];
+ let post_str = &parent_str[post_range - parent.text_range().start()];
+ let new: String = vec.iter().map(|it| it.to_string()).collect();
+
+ write!(f, "{pre_str}\x1b[41m{old_str}\x1b[42m{new}\x1b[0m\x1b[K{post_str}")
+ }
+ }
+ }
+}
+
/// Utility trait to allow calling syntax editor functions with references or owned
/// nodes. Do not use outside of this module.
pub trait Element {
diff --git a/crates/syntax/src/syntax_editor/edit_algo.rs b/crates/syntax/src/syntax_editor/edit_algo.rs
index 57ecbe5701..d6d903715d 100644
--- a/crates/syntax/src/syntax_editor/edit_algo.rs
+++ b/crates/syntax/src/syntax_editor/edit_algo.rs
@@ -1,9 +1,14 @@
//! Implementation of applying changes to a syntax tree.
-use std::{cmp::Ordering, collections::VecDeque, ops::RangeInclusive};
+use std::{
+ cmp::Ordering,
+ collections::VecDeque,
+ ops::{Range, RangeInclusive},
+};
use rowan::TextRange;
use rustc_hash::FxHashMap;
+use stdx::format_to;
use crate::{
syntax_editor::{mapping::MissingMapping, Change, ChangeKind, PositionRepr},
@@ -76,11 +81,9 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
|| (l.target_range().end() <= r.target_range().start())
});
- if stdx::never!(
- !disjoint_replaces_ranges,
- "some replace change ranges intersect: {:?}",
- changes
- ) {
+ if !disjoint_replaces_ranges {
+ report_intersecting_changes(&changes, get_node_depth, &root);
+
return SyntaxEdit {
old_root: root.clone(),
new_root: root,
@@ -293,6 +296,78 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
}
}
+fn report_intersecting_changes(
+ changes: &[Change],
+ mut get_node_depth: impl FnMut(rowan::SyntaxNode<crate::RustLanguage>) -> usize,
+ root: &rowan::SyntaxNode<crate::RustLanguage>,
+) {
+ let intersecting_changes = changes
+ .iter()
+ .zip(changes.iter().skip(1))
+ .filter(|(l, r)| {
+ // We only care about checking for disjoint replace ranges.
+ matches!(
+ (l.change_kind(), r.change_kind()),
+ (
+ ChangeKind::Replace | ChangeKind::ReplaceRange,
+ ChangeKind::Replace | ChangeKind::ReplaceRange
+ )
+ )
+ })
+ .filter(|(l, r)| {
+ get_node_depth(l.target_parent()) == get_node_depth(r.target_parent())
+ && (l.target_range().end() > r.target_range().start())
+ });
+
+ let mut error_msg = String::from("some replace change ranges intersect!\n");
+
+ let parent_str = root.to_string();
+
+ for (l, r) in intersecting_changes {
+ let mut highlighted_str = parent_str.clone();
+ let l_range = l.target_range();
+ let r_range = r.target_range();
+
+ let i_range = l_range.intersect(r_range).unwrap();
+ let i_str = format!("\x1b[46m{}", &parent_str[i_range]);
+
+ let pre_range: Range<usize> = l_range.start().into()..i_range.start().into();
+ let pre_str = format!("\x1b[44m{}", &parent_str[pre_range]);
+
+ let (highlight_range, highlight_str) = if l_range == r_range {
+ format_to!(error_msg, "\x1b[46mleft change:\x1b[0m {l:?} {l}\n");
+ format_to!(error_msg, "\x1b[46mequals\x1b[0m\n");
+ format_to!(error_msg, "\x1b[46mright change:\x1b[0m {r:?} {r}\n");
+ let i_highlighted = format!("{i_str}\x1b[0m\x1b[K");
+ let total_range: Range<usize> = i_range.into();
+ (total_range, i_highlighted)
+ } else {
+ format_to!(error_msg, "\x1b[44mleft change:\x1b[0m {l:?} {l}\n");
+ let range_end = if l_range.contains_range(r_range) {
+ format_to!(error_msg, "\x1b[46mcovers\x1b[0m\n");
+ format_to!(error_msg, "\x1b[46mright change:\x1b[0m {r:?} {r}\n");
+ l_range.end()
+ } else {
+ format_to!(error_msg, "\x1b[46mintersects\x1b[0m\n");
+ format_to!(error_msg, "\x1b[42mright change:\x1b[0m {r:?} {r}\n");
+ r_range.end()
+ };
+
+ let post_range: Range<usize> = i_range.end().into()..range_end.into();
+
+ let post_str = format!("\x1b[42m{}", &parent_str[post_range]);
+ let result = format!("{pre_str}{i_str}{post_str}\x1b[0m\x1b[K");
+ let total_range: Range<usize> = l_range.start().into()..range_end.into();
+ (total_range, result)
+ };
+ highlighted_str.replace_range(highlight_range, &highlight_str);
+
+ format_to!(error_msg, "{highlighted_str}\n");
+ }
+
+ stdx::always!(false, "{}", error_msg);
+}
+
fn to_owning_node(element: &SyntaxElement) -> SyntaxNode {
match element {
SyntaxElement::Node(node) => node.clone(),
diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs
index e7279fa1f6..d3afac8501 100644
--- a/crates/test-utils/src/lib.rs
+++ b/crates/test-utils/src/lib.rs
@@ -421,8 +421,8 @@ pub fn format_diff(chunks: Vec<dissimilar::Chunk<'_>>) -> String {
for chunk in chunks {
let formatted = match chunk {
dissimilar::Chunk::Equal(text) => text.into(),
- dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m"),
- dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m"),
+ dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m\x1b[K"),
+ dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m\x1b[K"),
};
buf.push_str(&formatted);
}