mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 21:28:21 +00:00
Fix replacement edit range computation (#12171)
## Summary This PR fixes various bugs for computing the replacement range between the original and modified source for the language server. 1. When finding the end offset of the source and modified range, we should apply `zip` on the reversed iterator. The bug was that it was reversing the already zipped iterator. The problem here is that the length of both slices aren't going to be the same unless the source wasn't modified at all. Refer to the [Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=44f860d31bd26456f3586b6ab530c22f) where you can see this in action. 2. Skip the first line when computing the start offset because the first line start value will always be 0 and the default value of the source / modified range start is also 0. So, comparing 0 and 0 is not useful which means we can skip the first value. 3. While iterating in the reverse direction, we should only stop if the line start is strictly less than the source start i.e., we should use `<` instead of `<=`. fixes: #12128 ## Test Plan Add test cases where the text is being inserted, deleted, and replaced between the original and new source code, validate the replacement ranges.
This commit is contained in:
parent
8210c1ed5b
commit
d870720841
1 changed files with 178 additions and 48 deletions
|
@ -1,5 +1,6 @@
|
|||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Replacement {
|
||||
pub(crate) source_range: TextRange,
|
||||
pub(crate) modified_range: TextRange,
|
||||
|
@ -15,41 +16,46 @@ impl Replacement {
|
|||
modified_line_starts: &[TextSize],
|
||||
) -> Self {
|
||||
let mut source_start = TextSize::default();
|
||||
let mut replaced_start = TextSize::default();
|
||||
let mut source_end = source.text_len();
|
||||
let mut replaced_end = modified.text_len();
|
||||
let mut line_iter = source_line_starts
|
||||
let mut modified_start = TextSize::default();
|
||||
|
||||
for (source_line_start, modified_line_start) in source_line_starts
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(modified_line_starts.iter().copied());
|
||||
for (source_line_start, modified_line_start) in line_iter.by_ref() {
|
||||
if source_line_start != modified_line_start
|
||||
|| source[TextRange::new(source_start, source_line_start)]
|
||||
!= modified[TextRange::new(replaced_start, modified_line_start)]
|
||||
.zip(modified_line_starts.iter().copied())
|
||||
.skip(1)
|
||||
{
|
||||
if source[TextRange::new(source_start, source_line_start)]
|
||||
!= modified[TextRange::new(modified_start, modified_line_start)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_start = source_line_start;
|
||||
replaced_start = modified_line_start;
|
||||
modified_start = modified_line_start;
|
||||
}
|
||||
|
||||
let mut line_iter = line_iter.rev();
|
||||
let mut source_end = source.text_len();
|
||||
let mut modified_end = modified.text_len();
|
||||
|
||||
for (old_line_start, new_line_start) in line_iter.by_ref() {
|
||||
if old_line_start <= source_start
|
||||
|| new_line_start <= replaced_start
|
||||
|| source[TextRange::new(old_line_start, source_end)]
|
||||
!= modified[TextRange::new(new_line_start, replaced_end)]
|
||||
for (source_line_start, modified_line_start) in source_line_starts
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.zip(modified_line_starts.iter().rev().copied())
|
||||
{
|
||||
if source_line_start < source_start
|
||||
|| modified_line_start < modified_start
|
||||
|| source[TextRange::new(source_line_start, source_end)]
|
||||
!= modified[TextRange::new(modified_line_start, modified_end)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_end = old_line_start;
|
||||
replaced_end = new_line_start;
|
||||
source_end = source_line_start;
|
||||
modified_end = modified_line_start;
|
||||
}
|
||||
|
||||
Replacement {
|
||||
source_range: TextRange::new(source_start, source_end),
|
||||
modified_range: TextRange::new(replaced_start, replaced_end),
|
||||
modified_range: TextRange::new(modified_start, modified_end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,42 +63,166 @@ impl Replacement {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use super::Replacement;
|
||||
|
||||
#[test]
|
||||
fn find_replacement_range_works() {
|
||||
let original = r#"
|
||||
aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
eeee
|
||||
"#;
|
||||
let original_index = LineIndex::from_source_text(original);
|
||||
let new = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
let new_index = LineIndex::from_source_text(new);
|
||||
let expected = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
fn compute_replacement(source: &str, modified: &str) -> (Replacement, String) {
|
||||
let source_index = LineIndex::from_source_text(source);
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
let replacement = Replacement::between(
|
||||
original,
|
||||
original_index.line_starts(),
|
||||
new,
|
||||
new_index.line_starts(),
|
||||
source,
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
let mut test = original.to_string();
|
||||
test.replace_range(
|
||||
let mut expected = source.to_string();
|
||||
expected.replace_range(
|
||||
replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(),
|
||||
&new[replacement.modified_range],
|
||||
&modified[replacement.modified_range],
|
||||
);
|
||||
(replacement, expected)
|
||||
}
|
||||
|
||||
assert_eq!(expected, &test);
|
||||
#[test]
|
||||
fn delete_first_line() {
|
||||
let source = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
";
|
||||
let modified = "bbbb
|
||||
cccc
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(replacement.source_range, TextRange::new(0.into(), 5.into()));
|
||||
assert_eq!(replacement.modified_range, TextRange::empty(0.into()));
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_middle_line() {
|
||||
let source = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
";
|
||||
let modified = "aaaa
|
||||
bbbb
|
||||
dddd
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(
|
||||
replacement.source_range,
|
||||
TextRange::new(10.into(), 15.into())
|
||||
);
|
||||
assert_eq!(replacement.modified_range, TextRange::empty(10.into()));
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_multiple_lines() {
|
||||
let source = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
eeee
|
||||
ffff
|
||||
";
|
||||
let modified = "aaaa
|
||||
cccc
|
||||
dddd
|
||||
ffff
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(
|
||||
replacement.source_range,
|
||||
TextRange::new(5.into(), 25.into())
|
||||
);
|
||||
assert_eq!(
|
||||
replacement.modified_range,
|
||||
TextRange::new(5.into(), 15.into())
|
||||
);
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_first_line() {
|
||||
let source = "bbbb
|
||||
cccc
|
||||
";
|
||||
let modified = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(replacement.source_range, TextRange::empty(0.into()));
|
||||
assert_eq!(
|
||||
replacement.modified_range,
|
||||
TextRange::new(0.into(), 5.into())
|
||||
);
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_middle_line() {
|
||||
let source = "aaaa
|
||||
cccc
|
||||
";
|
||||
let modified = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(replacement.source_range, TextRange::empty(5.into()));
|
||||
assert_eq!(
|
||||
replacement.modified_range,
|
||||
TextRange::new(5.into(), 10.into())
|
||||
);
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_multiple_lines() {
|
||||
let source = "aaaa
|
||||
cccc
|
||||
eeee
|
||||
";
|
||||
let modified = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(
|
||||
replacement.source_range,
|
||||
TextRange::new(5.into(), 15.into())
|
||||
);
|
||||
assert_eq!(
|
||||
replacement.modified_range,
|
||||
TextRange::new(5.into(), 20.into())
|
||||
);
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_lines() {
|
||||
let source = "aaaa
|
||||
bbbb
|
||||
cccc
|
||||
";
|
||||
let modified = "aaaa
|
||||
bbcb
|
||||
cccc
|
||||
";
|
||||
let (replacement, expected) = compute_replacement(source, modified);
|
||||
assert_eq!(
|
||||
replacement.source_range,
|
||||
TextRange::new(5.into(), 10.into())
|
||||
);
|
||||
assert_eq!(
|
||||
replacement.modified_range,
|
||||
TextRange::new(5.into(), 10.into())
|
||||
);
|
||||
assert_eq!(modified, &expected);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue