Skip to content

Commit f7adafc

Browse files
committed
internal: Improve reporting of intersecting changes
1 parent 16402de commit f7adafc

File tree

3 files changed

+142
-8
lines changed

3 files changed

+142
-8
lines changed

src/tools/rust-analyzer/crates/syntax/src/syntax_editor.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! [`SyntaxEditor`]: https://github.com/dotnet/roslyn/blob/43b0b05cc4f492fd5de00f6f6717409091df8daa/src/Workspaces/Core/Portable/Editing/SyntaxEditor.cs
66
77
use std::{
8+
fmt,
89
num::NonZeroU32,
910
ops::RangeInclusive,
1011
sync::atomic::{AtomicU32, Ordering},
@@ -282,6 +283,64 @@ enum ChangeKind {
282283
Replace,
283284
}
284285

286+
impl fmt::Display for Change {
287+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288+
match self {
289+
Change::Insert(position, node_or_token) => {
290+
let parent = position.parent();
291+
let mut parent_str = parent.to_string();
292+
let target_range = self.target_range().start() - parent.text_range().start();
293+
294+
parent_str.insert_str(
295+
target_range.into(),
296+
&format!("\x1b[42m{node_or_token}\x1b[0m\x1b[K"),
297+
);
298+
f.write_str(&parent_str)
299+
}
300+
Change::InsertAll(position, vec) => {
301+
let parent = position.parent();
302+
let mut parent_str = parent.to_string();
303+
let target_range = self.target_range().start() - parent.text_range().start();
304+
let insertion: String = vec.iter().map(|it| it.to_string()).collect();
305+
306+
parent_str
307+
.insert_str(target_range.into(), &format!("\x1b[42m{insertion}\x1b[0m\x1b[K"));
308+
f.write_str(&parent_str)
309+
}
310+
Change::Replace(old, new) => {
311+
if let Some(new) = new {
312+
write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
313+
} else {
314+
write!(f, "\x1b[41m{old}\x1b[0m\x1b[K")
315+
}
316+
}
317+
Change::ReplaceWithMany(old, vec) => {
318+
let new: String = vec.iter().map(|it| it.to_string()).collect();
319+
write!(f, "\x1b[41m{old}\x1b[42m{new}\x1b[0m\x1b[K")
320+
}
321+
Change::ReplaceAll(range, vec) => {
322+
let parent = range.start().parent().unwrap();
323+
let parent_str = parent.to_string();
324+
let pre_range =
325+
TextRange::new(parent.text_range().start(), range.start().text_range().start());
326+
let old_range = TextRange::new(
327+
range.start().text_range().start(),
328+
range.end().text_range().end(),
329+
);
330+
let post_range =
331+
TextRange::new(range.end().text_range().end(), parent.text_range().end());
332+
333+
let pre_str = &parent_str[pre_range - parent.text_range().start()];
334+
let old_str = &parent_str[old_range - parent.text_range().start()];
335+
let post_str = &parent_str[post_range - parent.text_range().start()];
336+
let new: String = vec.iter().map(|it| it.to_string()).collect();
337+
338+
write!(f, "{pre_str}\x1b[41m{old_str}\x1b[42m{new}\x1b[0m\x1b[K{post_str}")
339+
}
340+
}
341+
}
342+
}
343+
285344
/// Utility trait to allow calling syntax editor functions with references or owned
286345
/// nodes. Do not use outside of this module.
287346
pub trait Element {

src/tools/rust-analyzer/crates/syntax/src/syntax_editor/edit_algo.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
//! Implementation of applying changes to a syntax tree.
22
3-
use std::{cmp::Ordering, collections::VecDeque, ops::RangeInclusive};
3+
use std::{
4+
cmp::Ordering,
5+
collections::VecDeque,
6+
ops::{Range, RangeInclusive},
7+
};
48

59
use rowan::TextRange;
610
use rustc_hash::FxHashMap;
11+
use stdx::format_to;
712

813
use crate::{
914
syntax_editor::{mapping::MissingMapping, Change, ChangeKind, PositionRepr},
@@ -76,11 +81,9 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
7681
|| (l.target_range().end() <= r.target_range().start())
7782
});
7883

79-
if stdx::never!(
80-
!disjoint_replaces_ranges,
81-
"some replace change ranges intersect: {:?}",
82-
changes
83-
) {
84+
if !disjoint_replaces_ranges {
85+
report_intersecting_changes(&changes, get_node_depth, &root);
86+
8487
return SyntaxEdit {
8588
old_root: root.clone(),
8689
new_root: root,
@@ -293,6 +296,78 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
293296
}
294297
}
295298

299+
fn report_intersecting_changes(
300+
changes: &[Change],
301+
mut get_node_depth: impl FnMut(rowan::SyntaxNode<crate::RustLanguage>) -> usize,
302+
root: &rowan::SyntaxNode<crate::RustLanguage>,
303+
) {
304+
let intersecting_changes = changes
305+
.iter()
306+
.zip(changes.iter().skip(1))
307+
.filter(|(l, r)| {
308+
// We only care about checking for disjoint replace ranges.
309+
matches!(
310+
(l.change_kind(), r.change_kind()),
311+
(
312+
ChangeKind::Replace | ChangeKind::ReplaceRange,
313+
ChangeKind::Replace | ChangeKind::ReplaceRange
314+
)
315+
)
316+
})
317+
.filter(|(l, r)| {
318+
get_node_depth(l.target_parent()) == get_node_depth(r.target_parent())
319+
&& (l.target_range().end() > r.target_range().start())
320+
});
321+
322+
let mut error_msg = String::from("some replace change ranges intersect!\n");
323+
324+
let parent_str = root.to_string();
325+
326+
for (l, r) in intersecting_changes {
327+
let mut highlighted_str = parent_str.clone();
328+
let l_range = l.target_range();
329+
let r_range = r.target_range();
330+
331+
let i_range = l_range.intersect(r_range).unwrap();
332+
let i_str = format!("\x1b[46m{}", &parent_str[i_range]);
333+
334+
let pre_range: Range<usize> = l_range.start().into()..i_range.start().into();
335+
let pre_str = format!("\x1b[44m{}", &parent_str[pre_range]);
336+
337+
let (highlight_range, highlight_str) = if l_range == r_range {
338+
format_to!(error_msg, "\x1b[46mleft change:\x1b[0m {l:?} {l}\n");
339+
format_to!(error_msg, "\x1b[46mequals\x1b[0m\n");
340+
format_to!(error_msg, "\x1b[46mright change:\x1b[0m {r:?} {r}\n");
341+
let i_highlighted = format!("{i_str}\x1b[0m\x1b[K");
342+
let total_range: Range<usize> = i_range.into();
343+
(total_range, i_highlighted)
344+
} else {
345+
format_to!(error_msg, "\x1b[44mleft change:\x1b[0m {l:?} {l}\n");
346+
let range_end = if l_range.contains_range(r_range) {
347+
format_to!(error_msg, "\x1b[46mcovers\x1b[0m\n");
348+
format_to!(error_msg, "\x1b[46mright change:\x1b[0m {r:?} {r}\n");
349+
l_range.end()
350+
} else {
351+
format_to!(error_msg, "\x1b[46mintersects\x1b[0m\n");
352+
format_to!(error_msg, "\x1b[42mright change:\x1b[0m {r:?} {r}\n");
353+
r_range.end()
354+
};
355+
356+
let post_range: Range<usize> = i_range.end().into()..range_end.into();
357+
358+
let post_str = format!("\x1b[42m{}", &parent_str[post_range]);
359+
let result = format!("{pre_str}{i_str}{post_str}\x1b[0m\x1b[K");
360+
let total_range: Range<usize> = l_range.start().into()..range_end.into();
361+
(total_range, result)
362+
};
363+
highlighted_str.replace_range(highlight_range, &highlight_str);
364+
365+
format_to!(error_msg, "{highlighted_str}\n");
366+
}
367+
368+
stdx::always!(false, "{}", error_msg);
369+
}
370+
296371
fn to_owning_node(element: &SyntaxElement) -> SyntaxNode {
297372
match element {
298373
SyntaxElement::Node(node) => node.clone(),

src/tools/rust-analyzer/crates/test-utils/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,8 @@ pub fn format_diff(chunks: Vec<dissimilar::Chunk<'_>>) -> String {
421421
for chunk in chunks {
422422
let formatted = match chunk {
423423
dissimilar::Chunk::Equal(text) => text.into(),
424-
dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m"),
425-
dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m"),
424+
dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m\x1b[K"),
425+
dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m\x1b[K"),
426426
};
427427
buf.push_str(&formatted);
428428
}

0 commit comments

Comments
 (0)