Unnamed repository; edit this file 'description' to name the repository.
internal: Improve reporting of intersecting changes
| -rw-r--r-- | crates/syntax/src/syntax_editor.rs | 59 | ||||
| -rw-r--r-- | crates/syntax/src/syntax_editor/edit_algo.rs | 87 | ||||
| -rw-r--r-- | crates/test-utils/src/lib.rs | 4 |
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); } |