|
| 1 | +use std::borrow::Cow; |
| 2 | +use std::num::NonZeroUsize; |
| 3 | + |
| 4 | +use anstyle::Style; |
| 5 | +use similar::{ChangeTag, TextDiff}; |
| 6 | + |
1 | 7 | use ruff_annotate_snippets::Renderer as AnnotateRenderer;
|
| 8 | +use ruff_diagnostics::{Applicability, Fix}; |
| 9 | +use ruff_source_file::OneIndexed; |
| 10 | +use ruff_text_size::{Ranged, TextRange, TextSize}; |
2 | 11 |
|
3 | 12 | use crate::diagnostic::render::{FileResolver, Resolved};
|
4 |
| -use crate::diagnostic::{Diagnostic, DisplayDiagnosticConfig, stylesheet::DiagnosticStylesheet}; |
| 13 | +use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled}; |
| 14 | +use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig}; |
5 | 15 |
|
6 | 16 | pub(super) struct FullRenderer<'a> {
|
7 | 17 | resolver: &'a dyn FileResolver,
|
@@ -48,12 +58,199 @@ impl<'a> FullRenderer<'a> {
|
48 | 58 | writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
49 | 59 | }
|
50 | 60 | writeln!(f)?;
|
| 61 | + |
| 62 | + if self.config.show_fix_diff { |
| 63 | + if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) { |
| 64 | + writeln!(f, "{diff}")?; |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + Ok(()) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +/// Renders a diff that shows the code fixes. |
| 74 | +/// |
| 75 | +/// The implementation isn't fully fledged out and only used by tests. Before using in production, try |
| 76 | +/// * Improve layout |
| 77 | +/// * Replace tabs with spaces for a consistent experience across terminals |
| 78 | +/// * Replace zero-width whitespaces |
| 79 | +/// * Print a simpler diff if only a single line has changed |
| 80 | +/// * Compute the diff from the `Edit` because diff calculation is expensive. |
| 81 | +struct Diff<'a> { |
| 82 | + fix: &'a Fix, |
| 83 | + diagnostic_source: DiagnosticSource, |
| 84 | + stylesheet: &'a DiagnosticStylesheet, |
| 85 | +} |
| 86 | + |
| 87 | +impl<'a> Diff<'a> { |
| 88 | + fn from_diagnostic( |
| 89 | + diagnostic: &'a Diagnostic, |
| 90 | + stylesheet: &'a DiagnosticStylesheet, |
| 91 | + resolver: &'a dyn FileResolver, |
| 92 | + ) -> Option<Diff<'a>> { |
| 93 | + Some(Diff { |
| 94 | + fix: diagnostic.fix()?, |
| 95 | + diagnostic_source: diagnostic |
| 96 | + .primary_span_ref()? |
| 97 | + .file |
| 98 | + .diagnostic_source(resolver), |
| 99 | + stylesheet, |
| 100 | + }) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +impl std::fmt::Display for Diff<'_> { |
| 105 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 106 | + let source_code = self.diagnostic_source.as_source_code(); |
| 107 | + let source_text = source_code.text(); |
| 108 | + |
| 109 | + // TODO(dhruvmanila): Add support for Notebook cells once it's user-facing |
| 110 | + let mut output = String::with_capacity(source_text.len()); |
| 111 | + let mut last_end = TextSize::default(); |
| 112 | + |
| 113 | + for edit in self.fix.edits() { |
| 114 | + output.push_str(source_code.slice(TextRange::new(last_end, edit.start()))); |
| 115 | + output.push_str(edit.content().unwrap_or_default()); |
| 116 | + last_end = edit.end(); |
| 117 | + } |
| 118 | + |
| 119 | + output.push_str(&source_text[usize::from(last_end)..]); |
| 120 | + |
| 121 | + let diff = TextDiff::from_lines(source_text, &output); |
| 122 | + |
| 123 | + let message = match self.fix.applicability() { |
| 124 | + // TODO(zanieb): Adjust this messaging once it's user-facing |
| 125 | + Applicability::Safe => "Safe fix", |
| 126 | + Applicability::Unsafe => "Unsafe fix", |
| 127 | + Applicability::DisplayOnly => "Display-only fix", |
| 128 | + }; |
| 129 | + |
| 130 | + // TODO(brent) `stylesheet.separator` is cyan rather than blue, as we had before. I think |
| 131 | + // we're getting rid of this soon anyway, so I didn't think it was worth adding another |
| 132 | + // style to the stylesheet temporarily. The color doesn't appear at all in the snapshot |
| 133 | + // tests, which is the only place these are currently used. |
| 134 | + writeln!(f, "ℹ {}", fmt_styled(message, self.stylesheet.separator))?; |
| 135 | + |
| 136 | + let (largest_old, largest_new) = diff |
| 137 | + .ops() |
| 138 | + .last() |
| 139 | + .map(|op| (op.old_range().start, op.new_range().start)) |
| 140 | + .unwrap_or_default(); |
| 141 | + |
| 142 | + let digit_with = OneIndexed::from_zero_indexed(largest_new.max(largest_old)).digits(); |
| 143 | + |
| 144 | + for (idx, group) in diff.grouped_ops(3).iter().enumerate() { |
| 145 | + if idx > 0 { |
| 146 | + writeln!(f, "{:-^1$}", "-", 80)?; |
| 147 | + } |
| 148 | + for op in group { |
| 149 | + for change in diff.iter_inline_changes(op) { |
| 150 | + let sign = match change.tag() { |
| 151 | + ChangeTag::Delete => "-", |
| 152 | + ChangeTag::Insert => "+", |
| 153 | + ChangeTag::Equal => " ", |
| 154 | + }; |
| 155 | + |
| 156 | + let line_style = LineStyle::from(change.tag(), self.stylesheet); |
| 157 | + |
| 158 | + let old_index = change.old_index().map(OneIndexed::from_zero_indexed); |
| 159 | + let new_index = change.new_index().map(OneIndexed::from_zero_indexed); |
| 160 | + |
| 161 | + write!( |
| 162 | + f, |
| 163 | + "{} {} |{}", |
| 164 | + Line { |
| 165 | + index: old_index, |
| 166 | + width: digit_with |
| 167 | + }, |
| 168 | + Line { |
| 169 | + index: new_index, |
| 170 | + width: digit_with |
| 171 | + }, |
| 172 | + fmt_styled(line_style.apply_to(sign), self.stylesheet.emphasis), |
| 173 | + )?; |
| 174 | + |
| 175 | + for (emphasized, value) in change.iter_strings_lossy() { |
| 176 | + let value = show_nonprinting(&value); |
| 177 | + if emphasized { |
| 178 | + write!( |
| 179 | + f, |
| 180 | + "{}", |
| 181 | + fmt_styled(line_style.apply_to(&value), self.stylesheet.underline) |
| 182 | + )?; |
| 183 | + } else { |
| 184 | + write!(f, "{}", line_style.apply_to(&value))?; |
| 185 | + } |
| 186 | + } |
| 187 | + if change.missing_newline() { |
| 188 | + writeln!(f)?; |
| 189 | + } |
| 190 | + } |
| 191 | + } |
51 | 192 | }
|
52 | 193 |
|
53 | 194 | Ok(())
|
54 | 195 | }
|
55 | 196 | }
|
56 | 197 |
|
| 198 | +struct LineStyle { |
| 199 | + style: Style, |
| 200 | +} |
| 201 | + |
| 202 | +impl LineStyle { |
| 203 | + fn apply_to(&self, input: &str) -> impl std::fmt::Display { |
| 204 | + fmt_styled(input, self.style) |
| 205 | + } |
| 206 | + |
| 207 | + fn from(value: ChangeTag, stylesheet: &DiagnosticStylesheet) -> LineStyle { |
| 208 | + match value { |
| 209 | + ChangeTag::Equal => LineStyle { |
| 210 | + style: stylesheet.none, |
| 211 | + }, |
| 212 | + ChangeTag::Delete => LineStyle { |
| 213 | + style: stylesheet.deletion, |
| 214 | + }, |
| 215 | + ChangeTag::Insert => LineStyle { |
| 216 | + style: stylesheet.insertion, |
| 217 | + }, |
| 218 | + } |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +struct Line { |
| 223 | + index: Option<OneIndexed>, |
| 224 | + width: NonZeroUsize, |
| 225 | +} |
| 226 | + |
| 227 | +impl std::fmt::Display for Line { |
| 228 | + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| 229 | + match self.index { |
| 230 | + None => { |
| 231 | + for _ in 0..self.width.get() { |
| 232 | + f.write_str(" ")?; |
| 233 | + } |
| 234 | + Ok(()) |
| 235 | + } |
| 236 | + Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()), |
| 237 | + } |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | +fn show_nonprinting(s: &str) -> Cow<'_, str> { |
| 242 | + if s.find(['\x07', '\x08', '\x1b', '\x7f']).is_some() { |
| 243 | + Cow::Owned( |
| 244 | + s.replace('\x07', "␇") |
| 245 | + .replace('\x08', "␈") |
| 246 | + .replace('\x1b', "␛") |
| 247 | + .replace('\x7f', "␡"), |
| 248 | + ) |
| 249 | + } else { |
| 250 | + Cow::Borrowed(s) |
| 251 | + } |
| 252 | +} |
| 253 | + |
57 | 254 | #[cfg(test)]
|
58 | 255 | mod tests {
|
59 | 256 | use ruff_diagnostics::Applicability;
|
|
0 commit comments