diff --git a/crates/rust-analyzer/src/cli/progress_report.rs b/crates/rust-analyzer/src/cli/progress_report.rs index 028311388c56..aff2c0d6ff52 100644 --- a/crates/rust-analyzer/src/cli/progress_report.rs +++ b/crates/rust-analyzer/src/cli/progress_report.rs @@ -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); @@ -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); + } +}