Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 78 additions & 27 deletions crates/rust-analyzer/src/cli/progress_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,7 @@ impl<'a> ProgressReport<'a> {
}

fn update_text(&mut self, text: &str) {
// Get length of common portion
let mut common_prefix_length = 0;
let common_length = usize::min(self.text.len(), text.len());

while common_prefix_length < common_length
&& text.chars().nth(common_prefix_length).unwrap()
== self.text.chars().nth(common_prefix_length).unwrap()
{
common_prefix_length += 1;
}

// Backtrack to the first differing character
let mut output = String::new();
output += &'\x08'.to_string().repeat(self.text.len() - common_prefix_length);
// Output new suffix, using chars() iter to ensure unicode compatibility
output.extend(text.chars().skip(common_prefix_length));

// If the new text is shorter than the old one: delete overlapping characters
if let Some(overlap_count) = self.text.len().checked_sub(text.len())
&& overlap_count > 0
{
output += &" ".repeat(overlap_count);
output += &"\x08".repeat(overlap_count);
}

let output = render_text_update(&self.text, text);
let _ = io::stdout().write(output.as_bytes());
let _ = io::stdout().flush();
text.clone_into(&mut self.text);
Expand All @@ -105,11 +81,86 @@ impl<'a> ProgressReport<'a> {
}

// Fill all last text to space and return the cursor
let spaces = " ".repeat(self.text.len());
let backspaces = "\x08".repeat(self.text.len());
let len = self.text.chars().count();
let spaces = " ".repeat(len);
let backspaces = "\x08".repeat(len);
print!("{backspaces}{spaces}{backspaces}");
let _ = io::stdout().flush();

self.text = String::new();
}
}

fn render_text_update(old: &str, new: &str) -> String {
let old_len = old.chars().count();
let new_len = new.chars().count();

// Get length of common portion
let mut common_prefix_length = 0;
let common_length = usize::min(old_len, new_len);

while common_prefix_length < common_length
&& new.chars().nth(common_prefix_length).unwrap()
== old.chars().nth(common_prefix_length).unwrap()
{
common_prefix_length += 1;
}

// Backtrack to the first differing character
let mut output = String::new();
output += &'\x08'.to_string().repeat(old_len - common_prefix_length);
// Output new suffix, using chars() iter to ensure unicode compatibility
output.extend(new.chars().skip(common_prefix_length));

// If the new text is shorter than the old one: delete overlapping characters
if let Some(overlap_count) = old_len.checked_sub(new_len)
&& overlap_count > 0
{
output += &" ".repeat(overlap_count);
output += &"\x08".repeat(overlap_count);
}

output
}

#[cfg(test)]
mod tests {
use super::render_text_update;

#[test]
fn ascii_prefix_reuse() {
let old = "1/7 14% processing: foo";
let new = "1/7 28% processing: bar";
let update = render_text_update(old, new);

let common = "1/7 ";
let backspaces = old.chars().count() - common.chars().count();
let expected = format!("{}{}", "\x08".repeat(backspaces), "28% processing: bar");
assert_eq!(update, expected);
}

#[test]
fn unicode_identifiers_do_not_panic() {
// Regression test for rust-lang/rust-analyzer#22844: previous code
// compared byte lengths with char indices, so `chars().nth(...).unwrap()`
// panicked on non-ASCII.
let old = "1/7 14% processing: f::消息";
let new = "2/7 28% processing: f::消息内容";
let update = render_text_update(old, new);

let backspaces = old.chars().count();
let expected = format!("{}{new}", "\x08".repeat(backspaces));
assert_eq!(update, expected);
}

#[test]
fn shorter_unicode_message_clears_overlap() {
let old = "processing: 消息内容";
let new = "processing: 消息";
let update = render_text_update(old, new);

// Drop the last two chars, then blank/backspace the leftover width.
let expected = "\x08\x08 \x08\x08";
assert_eq!(update, expected);
}
}