-
Notifications
You must be signed in to change notification settings - Fork 44
Customizable Trailing Margins for Snippets #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
For the Problem, could you include a code snippet and output for showing
Along with a potential example of what output you would like to accomplish? This will help in focusing on understanding the problem and exploring relevant considerations before getting too much into the details of the solutions. |
I modeled the above simulated "Current Output" section from actual output from code I'm actively developing that shows the output of two snippets joined by As for my particular use case, I generate a custom error type at compile-time via an attribute macro that stores all relevant information of the function to generate a I'll have some time on Friday/Saturday so I'll try to get a toy example posted then. |
Example from a testing development crate to create context rich errors for Nom. It's a bit more than a toy example, but I think an example of the minimum use case is needed to illustrate the point. Function#[annotate_error] // This generates the ContextError in the next section at compile-time
pub fn tuple_error_example<'a>(
input: &'a str,
) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {
nom::sequence::tuple((
nom::bytes::complete::tag("world"),
bytes::complete::tag("hello"),
tag("x"),
))(input)
} ErrorThe See Full ErrorContextError{
context: FunctionContext {
signature: SignatureSourceCapture {
source_text: "pub fn tuple_error_example<'a> (\n input: &'a str\n) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {",
line_number: 30, // this is in a testing crate so it starts here
},
parser_contexts: None, // This is for single parsers that don't have subparsers. Can be ignored for this example
combinator_parser_contexts: Some(
[
CombinatorParserSourceCapture {
binding_pattern: None,
ident: SourceCapture {
source_text: "nom::sequence::tuple",
line_number: 33,
start_column: Some(
4,
),
end_column: Some(
24,
),
span_length: Some(
20,
),
},
sub_parsers: Some(
[
ParserSourceCapture {
binding_pattern: None,
ident: SourceCapture {
source_text: "nom::bytes::complete::tag",
line_number: 34,
start_column: Some(
8,
),
end_column: Some(
33,
),
span_length: Some(
25,
),
},
pattern: SourceCapture {
source_text: "(\"world\")",
line_number: 34,
start_column: Some(
33,
),
end_column: Some(
42,
),
span_length: Some(
9,
),
},
combinator_parsers: None,
input: SourceCapture { // Ignore. Some refinement still needed to remove unnecessary sections like this.
source_text: "",
line_number: 0,
start_column: None,
end_column: None,
span_length: None,
},
},
ParserSourceCapture {
binding_pattern: None,
ident: SourceCapture {
source_text: "bytes::complete::tag",
line_number: 35,
start_column: Some(
8,
),
end_column: Some(
28,
),
span_length: Some(
20,
),
},
pattern: SourceCapture {
source_text: "(\"hello\")",
line_number: 35,
start_column: Some(
28,
),
end_column: Some(
37,
),
span_length: Some(
9,
),
},
combinator_parsers: None,
input: SourceCapture { // Ignore. Some refinement still needed to remove unnecessary sections like this.
source_text: "",
line_number: 0,
start_column: None,
end_column: None,
span_length: None,
},
},
ParserSourceCapture {
binding_pattern: None,
ident: SourceCapture {
source_text: "tag",
line_number: 36,
start_column: Some(
8,
),
end_column: Some(
11,
),
span_length: Some(
3,
),
},
pattern: SourceCapture {
source_text: "(\"x\")",
line_number: 36,
start_column: Some(
11,
),
end_column: Some(
16,
),
span_length: Some(
5,
),
},
combinator_parsers: None,
input: SourceCapture { // Ignore. Some refinement still needed to remove unnecessary sections like this.
source_text: "",
line_number: 0,
start_column: None,
end_column: None,
span_length: None,
},
},
],
),
sub_parser_error_tracing_position: 0,
input: None,
input_source: Some(
SourceCapture {
source_text: "(input)",
line_number: 37,
start_column: Some(
6,
),
end_column: Some(
13,
),
span_length: Some(
7,
),
},
),
},
],
),
}
file: Some("crates/testing/main.rs")
input: None // this is determined at runtime
} Notes
Multisnippetimpl<'a> ContextError<'a> {
// used in Debug impl. Keep logic separated from release build formatting
#[cfg(debug_assertions)]
pub fn fmt_annotation(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Assume lot of brute forcing the values out of the ContextError for the variables in the snippets as this is still early development proof of concept work. When the error is triggered, the `sub_parser_error_tracing_position` is what determines which `ParserSourceCapture` is used for the last snippet so we only use the sub-parser that triggered the error and fold the rest. I can edit later to inclue it if necessary
Level::Error
.title("ContextError")
.snippet(
Snippet::source(signature_text.as_str())
.origin(self.file.unwrap_or("file information not found"))
.line_start(signature_line_number),
)
.snippet(
Snippet::source(Box::leak(combinator_text)) // Lifetime issues if using the &str directly. Doesn't affect the output being discussed
.line_start(combinator_line_number),
)
.snippet(
Snippet::source(Box::leak(failed_parser_source_text)) // Lifetime issues if using the &str directly. Doesn't affect the output being discussed
.line_start(failed_parser_source_line_number)
.annotation( // This annotation is applied directly to section that caused the error
Level::Error
.span(failed_parser_source_text_start_column..failed_parser_source_text_end_column)
.label("error occurred here"),
),
).footer(Level::Info.title(self.input.unwrap_or_default()))
} Outputerror: ContextError
--> crates/testing/src/main.rs
|
30 | pub fn tuple_error_example<'a> (
31 | input: &'a str
32 | ) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {
|
33 | nom::sequence::tuple(
|
35 | bytes::complete::tag("hello")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error occurred here
= info: Found:
input: "xhelloy"
Expected:
pattern: "hello"
Single Snippetimpl<'a> ContextError<'a> {
// used in Debug impl. Keep logic separated from release build formatting
#[cfg(debug_assertions)]
pub fn fmt_annotation(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Assume the same brute forcing to get the values out of the ContextError for the variables as the Multisnippet example
// except we now need to get the failed parser_source_text_line_number, calculate the difference between that and the combinator to then add in the appropriate number of newline characters to create the desired text.
let diff = failed_parser_source_text_line_number - combinator_line_number;
let text = if diff > 1 {
let diff_rep = "\n".repeat(diff);
format!("{signature_text}\n{combinator_text}{diff_rep}{failed_parser_source_text}")
} else {
format!("{signature_text}\n{combinator_text}\n{failed_parser_source_text}")
}
.into_boxed_str();
Level::Error
.title("ContextError")
.snippet(
Snippet::source(Box::leak(text))
.line_start(signature_line_number)
.annotation(
Level::Error
.span( // using the same information to point out the `bytes::complete::tag("hello")`
failed_parser_source_text_start_column
..failed_parser_source_text_end_column,
)
.label("error occurred here"),
),
).footer(Level::Info.title(self.input.unwrap_or_default()))
} OutputI'm not sure if there's another way to annotate to get it on line 35, but this is clearly not the expected outcome error: ContextError
|
30 | pub fn tuple_error_example<'a> (
| _________^
31 | | input: &'a str
| |____^ error occurred here
32 | ) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {
33 | nom::sequence::tuple(
34 |
35 | bytes::complete::tag("hello")
|
= info: Found:
input: "xhelloy"
Expected:
pattern: "hello" Manually changing the annotation span to: .annotation(Level::Error.span(0..32).label("error occurred here")) // length of the first line The new output is: error: ContextError
|
30 | pub fn tuple_error_example<'a> (
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error occurred here
31 | input: &'a str
32 | ) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {
33 | nom::sequence::tuple(
34 |
35 | bytes::complete::tag("hello")
|
= info: Found:
input: "xhelloy"
Expected:
pattern: "hello" This still doesn't utilize the annotation directly where the problem occurred Desired Outputerror: ContextError
--> crates/testing/src/main.rs
|
30 | pub fn tuple_error_example<'a> (
31 | input: &'a str
32 | ) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {
33 | nom::sequence::tuple(
...
35 | bytes::complete::tag("hello")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error occurred here
|
= info: Found:
input: "xhelloy"
Expected:
pattern: "hello" |
Rather than manually concatenating snippets and injecting fold indicators, I wonder if providing more control over folding would be a more general, useful feature. For example, if we could provide a way to annotate spans as being "visible" without indicators or a label. FYI it is a big help when reproduction cases are standalone, runnable code. |
Yeah, I understand that runnable code would have been best. I was having difficulty trying to think of an example that illustrates the challenges while keeping the context. The main points are:
This is a working example. It doesn't really capture the reasoning behind why the strings are split and recombined, but it works. use annotate_snippets::{Level, Renderer, Snippet};
fn main() {
// Most of the text in this example is captured at compile time and composed at the end into a `Message` with some runtime text capturing for the info message
let (signature_text,signature_line_number) = ("pub fn tuple_error_example<'a> (\n input: &'a str\n) -> IResult<&'a str, (&'a str, &'a str, &'a str), ContextError<'a>> {",30);
// Things are split like this because in the original context the different parts can be on different lines and that needs to be handled
let (combinator_text, combinator_line_number, combinator_start_column, combinator_end_column) =
("nom::sequence::tuple((", 33, 4, 24);
let subparsers = [
(
("nom::bytes::complete::tag", 34, 8, 33),
("(\"world\")", 34, 33, 42),
),
(
("bytes::complete::tag", 35, 8, 28),
("(\"hello\")", 35, 28, 37),
),
(("tag", 36, 8, 11), ("(\"x\")", 36, 11, 16)),
];
let subparser_tracing_position = 1; // This is determined at runtime to determine which subparser caused the issue
let failed_subparser = subparsers[subparser_tracing_position];
let (
(
failed_subparser_ident_text,
failed_subparser_ident_line_number,
failed_subparser_ident_start_column,
failed_subparser_ident_end_column,
),
(
failed_subparser_pattern_text,
failed_subparser_pattern_line_number,
failed_subparser_pattern_start_column,
failed_subparser_pattern_end_column,
),
) = failed_subparser;
let padded_failed_subparser_text = format!(
"{}{failed_subparser_ident_text}{failed_subparser_pattern_text}",
" ".repeat(failed_subparser_ident_start_column)
);
let file_name = "crates/test_annotate/main.rs";
let info_text = format!(
r#"Found:
{input_ident}: "{input_str}"
Expected:
pattern: {pattern}
"#,
input_ident = "input", // This is from the `(input)` in `tuple((..),..)(input)` which can be named anything by the dev and is captured in the ContextError
input_str = "xhello", // This comes from runtime where since the first parser was successful, the input goes from "worldxhello" to "xhelloy"
pattern = failed_subparser_pattern_text // Just to indicate that this is captured from the ContextError
);
let padded_combinator_text =
format!("{}{combinator_text}", " ".repeat(combinator_start_column));
let mutiline = Level::Error
.title("ContextError")
.snippets([
Snippet::source(signature_text)
.origin(file_name)
.line_start(signature_line_number),
Snippet::source(&padded_combinator_text).line_start(combinator_line_number),
Snippet::source(&padded_failed_subparser_text)
.line_start(failed_subparser_ident_line_number)
.annotation(
Level::Error
.span(
failed_subparser_ident_start_column
..failed_subparser_pattern_end_column,
)
.label("error occurred here"),
),
])
.footer(Level::Info.title(&info_text));
let diff = failed_subparser_ident_line_number - combinator_line_number;
let text = if diff > 1 {
let diff_rep = "\n".repeat(diff);
format!(
"{signature_text}\n{padded_combinator_text}{diff_rep}{padded_failed_subparser_text}"
)
} else {
format!("{signature_text}\n{combinator_text}\n{failed_subparser_ident_text}{failed_subparser_pattern_text}")
};
let single_snippet = Level::Error.title("ContextError").snippet(
Snippet::source(&text)
.origin(file_name)
.line_start(signature_line_number)
.annotation(Level::Error.span(0..32).label("error occurred here")),
);
let style = anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::BrightRed.into()));
let renderer = Renderer::styled().line_no(style);
let multiline_rendered_output = renderer.render(mutiline);
anstream::println!("{multiline_rendered_output}");
let single_snippet_rendered_output = renderer.render(single_snippet);
anstream::println!("{single_snippet_rendered_output}");
} |
After some more digging, I can shift the annotation to the correct position by setting the annotation span to that of the desired line. The solution in my case is to get and store the start of the span for the function itself. I would then need to subtract that span from the span range of the line to be annotated to align it. It doesn't end there. That new span would then need to be processed further to shift it by the number of missing bytes in the folded code or else we go beyond the end of the buffer. This seems like edge case nightmare land. I'm going to experiment with the |
Problem
Sometimes there is a need to perform modifications to the displayed error output in order to highlight specific parts of the source code to increase developers' understanding of where and why an error occurred. This may result in seeking to create multislice snippets for different sections of source code such that only the relevant information is displayed in the Error output with useful annotations.
Currently, the formatting between multiple snippets limits the usefulness of performing such an action in that there is a trailing line inserted after every snippet with a
'|'
in the margin, regardless of if it disrupts the flow of the original source code. The current solution would be to create and manipulate a monolithic&str
before inserting it into aSnippet::source("some monolithic &str")
; however, this requires significant understanding of formatting and places an unnecessarily large burden on developers to create such a&str
. This also makes maintainability more difficult as diagnosing issues with creating a monolithic&str
from conditionals would take more effort than targeted debugging of specific snippets in aMessage
.Proposed Solution
Allow developers to customize the trailing margin of snippets so that they can compose snippet fragments more flexibly.
Option 1
Create an enum for specifically supported trailing margin types
This would limit the available options such that stylistic cohesion can be maintained between projects.
Create a new field and new method for the Snippet type
Usage:
Current Output
Notice the extra space between lines 42 and 43
Output After Proposed Solution
Here, we have proper spacing between lines 42 and 43 and the fold indicator between lines 43 and 50
From initial observation
The
None
case in theformat_line
method forDisplaySet
handles the trailing margin for non-anonymized line numbers:Possible Solution:
Option 2
Create two distinct methods for handling trailing margins
The
.collapse_trailing_margin()
would handle the collapse logic and the.set_trailing_margin(<String>)
would allow users to insert anything into the margin. This option is more flexible, but will most likely fracture cohesiveness between projects in the ecosystem.Usage
Action Items
Discussion Points:
The text was updated successfully, but these errors were encountered: