use std::borrow::Cow;
use std::num::NonZeroUsize;
use similar::{ChangeTag, DiffOp, TextDiff};
use annotate_snippets::{
Group as AnnotateGroup, Level as AnnotateLevel, Renderer as AnnotateRenderer,
};
use ruff_diagnostics::{Applicability, Fix};
use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::diagnostic::render::{FileResolver, Resolved};
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
pub(super) struct FullRenderer<'a> {
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
}
impl<'a> FullRenderer<'a> {
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
Self { resolver, config }
}
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
let stylesheet = if self.config.color {
DiagnosticStylesheet::styled().hyperlinks(self.config.hyperlinks)
} else {
DiagnosticStylesheet::plain()
};
let mut renderer = if self.config.color {
AnnotateRenderer::styled()
} else {
AnnotateRenderer::plain()
}
.cut_indicator("…")
.anonymized_line_numbers(self.config.anonymized_line_numbers);
renderer = renderer
.error(stylesheet.error)
.warning(stylesheet.warning)
.info(stylesheet.info)
.note(stylesheet.note)
.help(stylesheet.help)
.line_num(stylesheet.line_no)
.emphasis(stylesheet.emphasis)
.none(stylesheet.none)
.hyperlink(stylesheet.hyperlink);
for diag in diagnostics {
if self.config.is_canceled() {
return Ok(());
}
let resolved = Resolved::new(self.resolver, diag, self.config);
let renderable = resolved.to_renderable(self.config);
for diag in renderable.diagnostics.iter() {
writeln!(f, "{}", renderer.render(&[diag.to_annotate()]))?;
}
if diag.has_applicable_fix(self.config.fix_applicability())
&& let Some(diff) =
Diff::from_diagnostic(diag, &stylesheet, self.resolver, self.config)
{
write!(f, "{diff}")?;
if let Some(applicability) = to_applicability_annotate(diff.fix) {
writeln!(f, "{}", renderer.render(&[applicability]))?;
}
}
writeln!(f)?;
}
Ok(())
}
}
const FIX_CONTEXT: usize = 1;
struct Diff<'a> {
fix: &'a Fix,
diagnostic_source: DiagnosticSource,
notebook_index: Option<NotebookIndex>,
stylesheet: &'a DiagnosticStylesheet,
merge_window: usize,
}
impl<'a> Diff<'a> {
fn from_diagnostic(
diagnostic: &'a Diagnostic,
stylesheet: &'a DiagnosticStylesheet,
resolver: &'a dyn FileResolver,
config: &DisplayDiagnosticConfig,
) -> Option<Diff<'a>> {
let file = &diagnostic.primary_span_ref()?.file;
Some(Diff {
fix: diagnostic.fix()?,
diagnostic_source: file.diagnostic_source(resolver),
notebook_index: resolver.notebook_index(file),
stylesheet,
merge_window: config.merge_window,
})
}
fn write(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let source_code = self.diagnostic_source.as_source_code();
let source_text = source_code.text();
let cell_ranges = self.cell_ranges();
for (cell_index, range) in cell_ranges {
let (range, line_offset) = if cell_index.is_none()
&& let Some(first) = self.fix.edits().first()
&& let Some(last) = self.fix.edits().last()
{
let start_line = source_code
.line_index(first.start())
.saturating_sub(DIFF_CONTEXT_WINDOW);
let last_source_line = source_code.line_index(source_text.text_len());
let end_line = source_code
.line_index(last.end())
.saturating_add(DIFF_CONTEXT_WINDOW)
.min(last_source_line);
(
TextRange::new(
source_code.line_start(start_line),
source_code.line_end(end_line),
),
start_line.to_zero_indexed(),
)
} else {
(range, 0)
};
let edits = self
.fix
.edits()
.iter()
.filter(|edit| range.contains_range(edit.range()))
.collect::<Vec<_>>();
if edits.is_empty() {
continue;
}
let input = source_code.slice(range);
let mut output = String::with_capacity(input.len());
let mut last_end = range.start();
for edit in edits {
output.push_str(source_code.slice(TextRange::new(last_end, edit.start())));
output.push_str(edit.content().unwrap_or_default());
last_end = edit.end();
}
output.push_str(&source_text[usize::from(last_end)..usize::from(range.end())]);
let diff = TextDiff::from_lines(input, &output);
let mut grouped_ops: Vec<Vec<DiffOp>> = Vec::new();
for group in diff.grouped_ops(FIX_CONTEXT) {
if let Some(previous) = grouped_ops.last_mut()
&& let Some(DiffOp::Equal { new_index, len, .. }) = previous.last_mut()
&& let [
DiffOp::Equal {
new_index: next_new_index,
len: next_len,
..
},
rest @ ..,
] = group.as_slice()
&& next_new_index.saturating_sub(*new_index + *len) <= self.merge_window
{
*len = next_new_index + next_len - *new_index;
previous.extend_from_slice(rest);
} else {
grouped_ops.push(group);
}
}
let last_op = grouped_ops.last().and_then(|group| group.last());
let largest_new = last_op
.map(|op| op.new_range().end + line_offset)
.unwrap_or_default();
let digit_with = OneIndexed::new(largest_new).unwrap_or_default().digits();
if let Some(cell_index) = cell_index {
writeln!(f, "{:>1$} cell {cell_index}", ":::", digit_with.get() + 3)?;
}
self.write_gutter(f, digit_with)?;
for (idx, group) in grouped_ops.iter().enumerate() {
if idx > 0 {
writeln!(f, "{:-^1$}", "-", 80)?;
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, style, line_no_style, index) = match change.tag() {
ChangeTag::Delete => (
"-",
self.stylesheet.deletion,
self.stylesheet.deletion_line_no,
None,
),
ChangeTag::Insert => (
"+",
self.stylesheet.insertion,
self.stylesheet.insertion_line_no,
change.new_index(),
),
ChangeTag::Equal => (
"|",
self.stylesheet.none,
self.stylesheet.line_no,
change.new_index(),
),
};
let line = Line {
index: index.map(|i| {
OneIndexed::from_zero_indexed(i).saturating_add(line_offset)
}),
width: digit_with,
};
write!(
f,
"{line} {sign}",
line = fmt_styled(line, self.stylesheet.line_no),
sign = fmt_styled(sign, line_no_style),
)?;
let mut needs_separator = true;
for (emphasized, value) in change.iter_strings_lossy() {
if needs_separator && !value.trim_end_matches(['\n', '\r']).is_empty() {
f.write_str(" ")?;
needs_separator = false;
}
let value = show_nonprinting(&value);
let styled = fmt_styled(value, style);
if emphasized {
write!(f, "{}", fmt_styled(styled, self.stylesheet.emphasis))?;
} else {
write!(f, "{styled}")?;
}
}
if change.missing_newline() {
writeln!(f)?;
}
}
}
}
self.write_gutter(f, digit_with)?;
}
Ok(())
}
fn cell_ranges(&self) -> Vec<(Option<usize>, TextRange)> {
let source_code = self.diagnostic_source.as_source_code();
let source_text = source_code.text();
let mut last_end = TextSize::ZERO;
let Some(notebook_index) = self.notebook_index.as_ref() else {
let offset = source_text.text_len();
let range = TextRange::new(last_end, offset);
return vec![(None, range)];
};
let mut last_cell_index = OneIndexed::MIN;
let mut cells: Vec<(Option<usize>, TextRange)> = Vec::new();
for cell in notebook_index.iter() {
if cell.cell_index() != last_cell_index {
let offset = source_code.line_start(cell.start_row());
let range = TextRange::new(last_end, offset);
cells.push((Some(last_cell_index.get()), range));
last_end = offset;
last_cell_index = cell.cell_index();
}
}
let offset = source_text.text_len();
let range = TextRange::new(last_end, offset);
cells.push((Some(last_cell_index.get()), range));
cells
}
fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result {
writeln!(
f,
"{line} {separator}",
line = fmt_styled(Line { index: None, width }, self.stylesheet.line_no),
separator = fmt_styled("|", self.stylesheet.line_no),
)
}
}
const DIFF_CONTEXT_WINDOW: usize = 3;
impl std::fmt::Display for Diff<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.write(f)
}
}
struct Line {
index: Option<OneIndexed>,
width: NonZeroUsize,
}
impl std::fmt::Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.index {
None => {
for _ in 0..self.width.get() {
f.write_str(" ")?;
}
Ok(())
}
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
}
}
}
fn show_nonprinting(s: &str) -> Cow<'_, str> {
if s.find(['\x07', '\x08', '\x1b', '\x7f']).is_some() {
Cow::Owned(
s.replace('\x07', "␇")
.replace('\x08', "␈")
.replace('\x1b', "␛")
.replace('\x7f', "␡"),
)
} else {
Cow::Borrowed(s)
}
}
fn to_applicability_annotate(fix: &Fix) -> Option<AnnotateGroup<'static>> {
let (level, message) = match fix.applicability() {
Applicability::Safe => return None,
Applicability::Unsafe => (
AnnotateLevel::WARNING,
"This is an unsafe fix and may change runtime behavior",
),
Applicability::DisplayOnly => (
AnnotateLevel::ERROR,
"This is a display-only fix and is likely to be incorrect",
),
};
let level = level.with_name("note");
Some(AnnotateGroup::with_title(level.primary_title(message)))
}
#[cfg(test)]
mod tests {
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::diagnostic::{
Annotation, DiagnosticFormat, Severity,
render::tests::{
NOTEBOOK, TestEnvironment, create_diagnostics, create_notebook_diagnostics,
create_syntax_error_diagnostics,
},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r###"
error[F401]: `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
help: Remove unused import: `os`
error[F841]: Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
error[F821]: Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
error[F821]: Undefined name `fibonaccii`
--> fib.py:12:16
|
10 | return 1
11 | else:
12 | return fibonaccii(n - 1) + fibonacci(n - 2)
| ^^^^^^^^^^ -
info: Did you mean to import it from `/some/path/def.py`?
--> fib.py:4:5
|
4 | def fibonacci(n):
| ^^^^^^^^^ `fibonacci` is defined here
5 | """Compute the nth number in the Fibonacci sequence."""
| ------------------------------------------------------- `fibonacci` is documented here
6 | x = 1
7 | if n == 0:
|
"###);
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[invalid-syntax]: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
error[invalid-syntax]: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|
");
}
#[test]
fn hide_severity_output() {
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
env.hide_severity(true);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
F401 [*] `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
help: Remove unused import: `os`
|
- import os
1 |
|
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
|
5 | """Compute the nth number in the Fibonacci sequence."""
- x = 1
6 +
7 | if n == 0:
|
note: This is an unsafe fix and may change runtime behavior
F821 Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
F821 Undefined name `fibonaccii`
--> fib.py:12:16
|
10 | return 1
11 | else:
12 | return fibonaccii(n - 1) + fibonacci(n - 2)
| ^^^^^^^^^^ -
info: Did you mean to import it from `/some/path/def.py`?
--> fib.py:4:5
|
4 | def fibonacci(n):
| ^^^^^^^^^ `fibonacci` is defined here
5 | """Compute the nth number in the Fibonacci sequence."""
| ------------------------------------------------------- `fibonacci` is documented here
6 | x = 1
7 | if n == 0:
|
"#);
}
#[test]
fn hide_severity_syntax_errors() {
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
env.hide_severity(true);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
invalid-syntax: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
invalid-syntax: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|
");
}
#[test]
fn empty_span_after_line_terminator() {
let mut env = TestEnvironment::new();
env.add(
"example.py",
r#"
if False:
print()
"#,
);
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"no-indented-block",
Severity::Error,
"Expected an indented block",
)
.primary("example.py", "3:0", "3:0", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[no-indented-block]: Expected an indented block
--> example.py:3:1
|
2 | if False:
3 | print()
| ^
");
}
#[test]
fn unprintable_characters() {
let mut env = TestEnvironment::new();
env.add("example.py", "nested_fstrings = f'{f'{f''}'}'");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1a" instead"#,
)
.primary("example.py", "1:24", "1:24", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead
--> example.py:1:25
|
1 | nested_fstrings = f'␈{f'␚{f'␛'}'}'
| ^
"#);
}
#[test]
fn multiple_unprintable_characters() -> std::io::Result<()> {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1a" instead"#,
)
.primary("example.py", "1:1", "1:1", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead
--> example.py:1:2
|
1 | ␈␚␛
| ^
"#);
Ok(())
}
#[test]
fn tab_replacement() {
let mut env = TestEnvironment::new();
env.add("example.py", "def foo():\n\treturn 1");
env.format(DiagnosticFormat::Full);
let diagnostic = env.err().primary("example.py", "2:1", "2:9", "").build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:2:2
|
1 | def foo():
2 | return 1
| ^^^^^^^^
");
}
#[test]
fn file_level() {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env.path("example.py").with_range(TextRange::default());
let mut annotation = Annotation::primary(span);
annotation.hide_snippet(true);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:1:1
");
}
#[test]
fn notebook_output() {
let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_status(true);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @"
error[F401][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
help: Remove unused import: `os`
::: cell 1
|
1 | # cell 1
- import os
|
error[F401][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
|
1 | # cell 2
- import math
2 |
|
error[F841]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
help: Remove assignment to unused variable `x`
");
}
#[test]
fn notebook_output_multiple_annotations() {
let mut env = TestEnvironment::new();
env.add("notebook.ipynb", NOTEBOOK);
let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "")
.secondary("notebook.ipynb", "4:7", "4:11", "second cell")
.help("Remove unused import: `os`")
.build(),
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "")
.secondary("notebook.ipynb", "10:4", "10:5", "second cell")
.help("Remove unused import: `os`")
.build(),
env.err()
.primary("notebook.ipynb", "4:7", "4:11", "second cell")
.secondary("notebook.ipynb", "6:0", "6:5", "print statement")
.help("Remove `print` statement")
.build(),
];
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
::: notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ---- second cell
3 |
4 | print('hello world')
|
help: Remove unused import: `os`
error[unused-import]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
::: notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| - second cell
help: Remove unused import: `os`
error[test-diagnostic]: main diagnostic message
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^ second cell
3 |
4 | print('hello world')
| ----- print statement
help: Remove `print` statement
");
}
#[test]
fn notebook_output_with_diff() {
let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
fn notebook_output_with_diff_spanning_cells() {
let (mut env, mut diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
let mut diagnostic = diagnostics.swap_remove(0);
let fix = diagnostic.fix_mut().unwrap();
let mut edits = fix.edits().to_vec();
for diag in diagnostics {
edits.extend_from_slice(diag.fix().unwrap().edits());
}
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic));
}
#[test]
fn normalize_carriage_return() {
let mut env = TestEnvironment::new();
env.add(
"example.py",
"# Keep parenthesis around preserved CR\rint(-\r 1)\rint(+\r 1)",
);
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env
.path("example.py")
.with_range(TextRange::at(TextSize::new(39), TextSize::new(0)));
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:2:1
|
1 | # Keep parenthesis around preserved CR
2 | int(-
| ^
3 | 1)
4 | int(+
|
");
}
#[test]
fn strip_bom() {
let mut env = TestEnvironment::new();
env.add("example.py", "\u{feff}import foo");
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env
.path("example.py")
.with_range(TextRange::at(TextSize::new(3), TextSize::new(0)));
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:1:1
|
1 | import foo
| ^
");
}
#[test]
fn bom_with_default_range() {
let mut env = TestEnvironment::new();
env.add("example.py", "\u{feff}import foo");
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env.path("example.py").with_range(TextRange::default());
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:1:1
|
1 | import foo
| ^
");
}
#[test]
fn end_of_file() {
let mut env = TestEnvironment::new();
let contents = "unexpected eof\n";
env.add("example.py", contents);
env.format(DiagnosticFormat::Full);
let mut diagnostic = env.err().build();
let span = env
.path("example.py")
.with_range(TextRange::at(contents.text_len(), TextSize::new(0)));
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:1:16
|
1 | unexpected eof
| ^
");
}
#[test]
fn longer_line_number_end_of_context() {
let mut env = TestEnvironment::new();
let contents = "\
line 1
line 2
line 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10
";
env.add("example.py", contents);
env.format(DiagnosticFormat::Full);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
let mut diagnostic = env.err().primary("example.py", "3", "3", "label").build();
diagnostic.help("Start of diff:");
let target = "line 7";
let line9 = contents.find(target).unwrap();
let range = TextRange::at(TextSize::try_from(line9).unwrap(), target.text_len());
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("fixed {target}"),
range,
)));
insta::assert_snapshot!(env.render(&diagnostic), @"
error[test-diagnostic][*]: main diagnostic message
--> example.py:3:1
|
1 | line 1
2 | line 2
3 | line 3
| ^^^^^^ label
4 | line 4
5 | line 5
|
help: Start of diff:
|
6 | line 6
- line 7
7 + fixed line 7
8 | line 8
|
note: This is an unsafe fix and may change runtime behavior
");
}
#[test]
fn nearby_fix_edits_share_diff_frame() {
let mut env = TestEnvironment::new();
let contents = "\
line 1
line 2
line 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10
line 11
line 12
line 13
";
env.add("example.py", contents);
env.format(DiagnosticFormat::Full);
env.context(0);
env.merge_window(2);
let replacement = |target: &str| {
let start = contents.find(target).unwrap();
Edit::range_replacement(
format!("fixed {target}"),
TextRange::at(TextSize::try_from(start).unwrap(), target.text_len()),
)
};
let mut diagnostic = env.err().primary("example.py", "2", "2", "").build();
diagnostic.help("Replace three lines");
diagnostic.set_fix(Fix::safe_edits(
replacement("line 2"),
[replacement("line 7"), replacement("line 13")],
));
insta::assert_snapshot!(env.render(&diagnostic), @"
error[test-diagnostic]: main diagnostic message
--> example.py:2:1
|
2 | line 2
| ^^^^^^
help: Replace three lines
|
1 | line 1
- line 2
2 + fixed line 2
3 | line 3
4 | line 4
5 | line 5
6 | line 6
- line 7
7 + fixed line 7
8 | line 8
--------------------------------------------------------------------------------
12 | line 12
- line 13
13 + fixed line 13
|
");
}
}