Description
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 a Snippet::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 a Message
.
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.
// Standardized. Easy to extend without breaking compatibility. Public to users.
pub enum TrailingMargin {
Bar, // `|` current style, used as default
Ellipsis, // either the unicode for ellipsis: `'\u{2026}'` or `"..."`. Considerations: different visual styling and unicode isn't supported in all fonts. Must trust fallback mechinisms to convert to "..." in the cases where '\u{2026}' isn't supported. Assumed low risk to breaking displayed output.
Collapse, // Remove any trailing margin so that two snippets are not separated by a new line. Useful for when logic is needed to alter two parts of the source differently without separating them with the default.
}
// Keeps the current style as the default
impl Default for TrailingMargin {
fn default() -> Self {
TrailingMargin::Bar
}
}
Create a new field and new method for the Snippet type
#[derive(Debug)]
pub struct Snippet<'a> {
pub(crate) origin: Option<&'a str>,
pub(crate) line_start: usize,
pub(crate) source: &'a str,
pub(crate) annotations: Vec<Annotation<'a>>,
pub(crate) trailing_margin: TrailingMargin, // add new field
pub(crate) fold: bool,
}
impl<'a> Snippet<'a> {
pub fn source(source: &'a str) -> Self {
Self {
origin: None,
line_start: 1,
source,
annotations: vec![],
trailing_margin: TrailingMargin::default(), // add new field
fold: false,
}
}
...
pub fn set_trailing_margin(mut self, trailing_margin_style: TrailingMargin) -> Self {
self.trailing_margin = trailing_margin_style;
self
}
}
Usage:
Level::Error
.title("Useful Error Header")
.snippet(
Snippet::source("pub fn example_function(testing: Option<&str>) -> Result<&str, CustomErrorThatDisplaysMessage> {")
.line_start(42)
.origin("crates/testing/src/main.rs")
.span(..) // assume correctness for example
.set_trailing_margin(TrailingMargin::Collapse) // the next snippet is on the next line; therefore, adding a trailing margin would create a disjunction with the source code. Possibility to infer this via the line_start data between two snippets.
)
.snippet(
Snippet::source(" if let Some(test) = testing {")
.line_start(43)
.span(..) // assume correctness for example
.set_trailing_margin(TrailingMargin::Ellipse) // There is code in the source between this snippet and the next, but it did not cause the error and can be collapsed. This style creates an indicator that there is code between the two snippets. Without the indicator, there may be confusion from overlooking the gap in line numbers. Better symbolic indication that there's folded code than the `|`
)
.snippet(
Snippet::source(" some source code where error occurred") // within the `if let` block, but the previous lines didn't cause the error
.line_start(50)
.span(..) // assume correctness for example
.annotation(
Level::Error
.span(..) // assume correctness for example
.label("error occurred here"),
)
.set_trailing_margin(TrailingMargin::Default) // Not necessary to include
)
Current Output
Notice the extra space between lines 42 and 43
error: Useful Error Header
--> crates/testing/src/main.rs
|
42 | pub fn example_function(testing: Option<&str>) -> Result<&str, CustomErrorThatDisplaysMessage> {
|
43 | if let Some(test) = testing {
|
50 | some source code where error occurred
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error occurred here
|
Output After Proposed Solution
Here, we have proper spacing between lines 42 and 43 and the fold indicator between lines 43 and 50
error: Useful Error Header
--> crates/testing/src/main.rs
|
42 | pub fn example_function(testing: Option<&str>) -> Result<&str, CustomErrorThatDisplaysMessage> {
43 | if let Some(test) = testing {
...
50 | some source code where error occurred
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error occurred here
|
From initial observation
The None
case in the format_line
method for DisplaySet
handles the trailing margin for non-anonymized line numbers:
impl DispaySet
fn format_line(dl: &DisplayLine<'_>,...) {
match dl {
DisplayLine::Source {line_no,...} => {
...
if anonymized_line_numbers && lineno.is_some() {
...
} else {
match lineno {
Some(n) => {
...
}
None => {
buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); // Here
}
}
}
}
};
}
Possible Solution:
impl DispaySet
fn format_line(dl: &DisplayLine<'_>,trailing_margin: TrailingMargin, ...) {
let lineno_color = stylesheet.line_no();
match dl {
DisplayLine::Source {line_no,...} => {
...
if anonymized_line_numbers && lineno.is_some() {
...
} else {
match lineno {
Some(n) => {
...
}
None => {
match trailing_margin {
TrailingMargin::Bar => buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color)
TrailingMargin::Ellipsis => buffer.putc(line_offset, lineno_width + 1, '\u{2026}', *lineno_color)
TrailingMargin::Collapse => {} // Don't modify the buffer
}
}
}
}
}
};
}
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
Level::Error
.title("Useful Error Header")
.snippet(
Snippet::source("pub fn example_function(testing:Option<&str>) -> Result<&str,CustomErrorThatDisplaysMessage> {")
.line_start(42)
.span(..) // assume correctness for example
.collapse_trailing_margin() // Specific method for collapsing
)
.snippet(
Snippet::source(" if let Some(test) = testing {")
.line_start(43)
.span(..) // assume correctness for example
.set_trailing_margin("...") // String. Could be anything
)
.snippet(
Snippet::source(" some source logic where error occurred") // within the `if let` block, but the previous lines didn't cause the error
.line_start(50)
.span(..) // assume correctness for example
.annotation(
Level::Error
.span(..) // assume correctness for example
.label("error occurred here"),
)
.set_trailing_margin("|") // Not necessary to include
)
Action Items
- Determine if a PR that will allow for trailing line margins to be modified by users will be accepted
- Determine which option is best:
- Option 1
- Option 2
- Alternative as determined by community
- Determine naming schemes
- Determine which areas should be impacted
- Add documentation
- Add examples
- Determine which option is best:
Discussion Points:
- Which option do you prefer and why?
- Are there additional margin types or behaviors that should be considered?
- Concerns about maintainability or compatibility with existing code?
- Does this change introduce any potential performance issues?
- Other considerations/concerns?