Skip to content

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

Closed
9 tasks
RodogInfinite opened this issue Feb 11, 2025 · 6 comments
Closed
9 tasks

Customizable Trailing Margins for Snippets #178

RodogInfinite opened this issue Feb 11, 2025 · 6 comments
Labels
C-enhancement Category: enhancement

Comments

@RodogInfinite
Copy link

RodogInfinite commented Feb 11, 2025

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

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?
@epage
Copy link
Contributor

epage commented Feb 12, 2025

For the Problem, could you include a code snippet and output for showing

  • the single snippet
  • the two snippets joined by |

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.

@epage epage added the C-enhancement Category: enhancement label Feb 12, 2025
@RodogInfinite
Copy link
Author

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 |. Also, by playing with the DisplaySet.format_line, I ended up with the simulated output in the "Output After Proposed Solution" section. I haven't actually implemented the proposed solution yet, just changed the indicated line in the "From initial observation" section.

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 Message. It's objective is to be a dev debugging tool so under the Debug implementation of the error type, the output of the Message changes based on the data in the custom error and the runtime conditions that trigger the error. Due to this, chaining Snippets together was the least painful solution when trying to combine everything together.

I'll have some time on Friday/Saturday so I'll try to get a toy example posted then.

@RodogInfinite
Copy link
Author

RodogInfinite commented Feb 18, 2025

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)
}

Error

The ContextError is created at compile-time by the annotate_error attribute macro and gathers all relevant information to create meaningful snippets. Expand details section below to view:

See Full Error
ContextError{
    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
  • An attempt was made to have Snippets or the entire Message directly in the ContextError but for various reason how Messages expect owned data and how the Renderer expects owned data caused issues .
  • The SourceCapture's source_text doesn't include the leading whitespace. This is added during the output formatting. It would be more straight forward to have another method on snippet that optionally allows the adding left padding based on the start column in the span

Multisnippet

impl<'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()))
    }

Output

error: 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"
  • Extra line between 32 and 33, between the signature and the first code line even though they're adjacent
  • Between 33 and 35, its easy to miss the discontinuity without a fold indicator

Single Snippet

impl<'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()))
}

Output

I'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 Output

error: 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"

@epage
Copy link
Contributor

epage commented Feb 18, 2025

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.

@RodogInfinite
Copy link
Author

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:

  1. When concatenating snippets, the annotation is applied to the specific line where the issue should be indicated. When manually concatenating the strings instead to just have a single snippet, the annotation is applied to the first line of the source. I haven't found an alternative approach that can shift the annotation position.
  2. The folding indication.

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}");
}

@RodogInfinite
Copy link
Author

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 Snippet type to make it more composable. That should have the same desired outcome and is probably a better solution than customizing the trailing margins. If that diverges from the goals of the project I'll go ahead and fork. Thank you for your quick responses. This discussion helped me view the problem in a new way. I'll go ahead and close this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: enhancement
Projects
None yet
Development

No branches or pull requests

2 participants