Skip to content

RFC: Proposal for "modern" API #13

Not planned
Not planned
@CAD97

Description

@CAD97

In order to address concerns in #11, #12, wycats/language-reporting#6, and probably others.

I've been experimenting with merging the APIs of codespan/language-reporting/annotate-snippets, and the below API surface is what that I think makes the most sense.

NOTE: the suggested API has changed multiple times from feedback, see conversation starting at this comment for the most recent API and discussion.

Original Proposal

An experimental implementation of the API based on #12 is at CAD97/retort#1 (being pushed within 24 hours of posting, I've got one last bit to "port" but I've got to get to bed now but I wanted to get this posted first).

EDIT: I've reconsidered this API, though the linked PR does implement most of it. I'm sketching a new slightly lower-level design from this one, and the diagnostic layout of this current API will probably be a wrapper library around annotate-snippets. (I get to use the retort name!)

API
use termcolor::WriteColor;

trait Span: fmt::Debug + Copy {
    type Origin: ?Sized + fmt::Debug + Eq;

    fn start(&self) -> usize;
    fn end(&self) -> usize;
    fn new(&self, start: usize, end: usize) -> Self;
    fn origin(&self) -> &Self::Origin;
}

trait SpanResolver<Sp> {
    fn first_line_of(&mut self, span: Sp) -> Option<SpannedLine<Sp>>;
    fn next_line_of(&mut self, span: Sp, line: SpannedLine<Sp>) -> Option<SpannedLine<Sp>>;
    fn write_span(&mut self, w: &mut dyn WriteColor, span: Sp) -> io::Result<()>;
    fn write_origin(&mut self, w: &mut dyn WriteColor, origin: Sp) -> io::Result<()>;
}

#[derive(Debug, Copy, Clone)]
pub struct SpannedLine<Sp> {
    line_num: usize,
    char_count: usize,
    span: Sp,
}

impl Span for (usize, usize) {
    type Origin = ();
}

impl<Sp: Span<Origin=()>> Span for (&'_ str, Sp) {
    type Origin = str;
}

impl<Sp: Span> SpanResolver<Sp> for &str
where Sp::Origin: fmt::Display;

mod diagnostic {
    #[derive(Debug, Clone)]
    struct Diagnostic<'a, Sp: Span> {
        pub primary: Annotation<'a, Sp>,
        pub code: Option<Cow<'a, str>>,
        pub secondary: Cow<'a, [Annotation<'a, Sp>]>,
    }

    #[derive(Debug, Clone)]
    struct Annotation<'a, Sp: Span> {
        pub span: Sp,
        pub level: Level,
        pub message: Cow<'a, str>,
    }

    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
    enum Level {
        Err,
        Warn,
        Info,
        Hint,
    }

    impl<Sp: Span> Diagnostic<'_, Sp> {
        pub fn borrow(&self) -> Diagnostic<'_, Sp>;
        pub fn into_owned(self) -> Diagnostic<'static, Sp>;
    }

    impl<Sp: Span> Annotation<'_, Sp> {
        pub fn borrow(&self) -> Annotation<'_, Sp>;
        pub fn into_owned(self) -> Annotation<'static, Sp>;
    }

    impl fmt::Display for Level;
}

mod style {
    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
    enum Mark {
        None,
        Start,
        Continue,
        End,
    }

    #[non_exhaustive]
    #[derive(Debug, Copy, Clone)]
    pub enum Style {
        Base,
        Code,
        Diagnostic(Level),
        LineNum,
        TitleLine,
        OriginLine,
    }

    trait Stylesheet {
        fn set_style(&mut self, w: &mut dyn WriteColor, style: Style) -> io::Result<()>;
        fn write_marks(&mut self, w: &mut dyn WriteColor, marks: &[Mark]) -> io::Result<()>;
        fn write_divider(&mut self, w: &mut dyn WriteColor) -> io::Result<()>;
        fn write_underline(
            &mut self,
            w: &mut dyn WriteColor,
            level: Level,
            len: usize,
        ) -> io::Result<()>;
    }

    struct Rustc; impl Stylesheet for Rustc;
    // other styles in the future
}

mod renderer {
    fn render<'a, Sp: Span>(
        w: &mut dyn WriteColor,
        stylesheet: &dyn Stylesheet,
        span_resolver: &mut dyn SpanResolver<Sp>,
        diagnostic: &'a Diagnostic<'a, Sp>,
    ) -> io::Result<()>;

    fn lsp<'a, Sp: Span + 'a>(
        diagnostics: impl IntoIterator<Item = Diagnostic<'a, Sp>>,
        source: Option<&'_ str>,
        span_resolver: impl FnMut(Sp) -> lsp_types::Location,
    ) -> Vec<lsp_types::PublishDiagnosticsParams>;
}

Notes:

  • I've skipped imports and implementation bodies for clarity. All definitions are exported where I've written them.
  • I've liberally used dyn Trait, so the only monomorphization should be over the Span type.
  • I'm not particularly attached to any of the organization of exports, things can move around.
  • Span::new is only used for impl SpanResolver<impl Span> for &str; making that impl more specific can get rid of that trait method.
  • SpanResolver takes &mut for its methods primarily because it can, in order to allow use of a single-threaded DB that requires &mut access for caching as a span resolver.
  • Span resolution is passed through SpanResolver at the last moment such that a SpanResolver can supply syntax highlighting for errors.
  • SpanResolver::write_origin only gets io::Write because styling is done ahead of time by Stylesheet. Because WriteColor does not have an upcast method, this means we can't use dyn WriteColor anywhere that will end up calling SpanResolver::write_origin. This can be changed to take WriteColor if desired.
  • Diagnostic's layout is tuned to have similar layout to the language server protocol's Diagnostic.
  • Diagnostic is set up so that Diagnostic<'_, Sp> can be borrowed but also an owned Diagnostic<'static, Sp> can be produced by using Cows. This eases use with constructed diagnostics.
  • Potential style improvement: extend Style::Code to be an enum of general code token types (e.g. the list from pygments), SpanResolver::write_span just gets the ability to set the style to one of those, which goes through the StyleSheet for styling.

Activity

CAD97

CAD97 commented on Oct 12, 2019

@CAD97
Author

cc list of potentially interested parties:

zbraniecki

zbraniecki commented on Oct 12, 2019

@zbraniecki
Contributor

Hi @CAD97 !

Thanks for taking a look at this and I'm excited to work together, if we end up deciding that we're aiming for the same shape of the API.

I did a first read of your proposal and only have very rough, unsorted initial thoughts to share so far. Most of them are critical, but please, do not read it as a general criticizm - it's just easier to focus on what I see as potentially incompatible. The code generally looks good!

Foundational Crate

You seem to use dependencies liberally. I'm of strong opinion that this functionality should be treated as a "foundational crate" per raphlinus terminology and as such should aim to maintain minimal, or even zero if possible, dependency tree. Your proposal has 22 dependencies, annotate-snippets has zero.
I'm open to add some, if we see a value in doing so, but I'd like to keep it at the very minimum and if possible look for dependencies that themselves don't introduce a long chain of dependencies in return.

API

Your API seems to resemble more imperative approach much closer than what I'm aiming for with annotate-snippets. The chain operations Diagnostics::build().code().level().primary().secondary() feels fairly awkward to me I must admit. I've been working at TC39 on JavaScript at the time when that model was very popular (jQuery!) and I'm not very convinced that it leads to a clean API use and highly-maintainable code (I'm not talking about our code, but the code that uses the API).

In principle, I see what I call Snippet struct as a data structure. Rust doesn't have a very good way to provide complex slash optional parameter list to constructor, but I came to conclusion that likely in this case we don't need it.

So, instead of making people write Snippet::new(title, description, level, code, &[secondary], ...) or your Snippet::build().title().description().level().code(), we can just expose ability to do Snippet { title: None, description: Some(...), level: Some("E203"), ... }.
It's very clean, well maintained, allows for omitting optional fields with Default trait, and what's most powerful, can be then wrapped in many different ways to construct an instance.

If I'm not mistaken, the only shortcoming of that approach is lack of inter-field validation allowing one to define a snippet with 5 lines, but an annotation on a sixth.

Initially, that led me to try to squeeze one or more constructors, because I dislike ability to produce internally inconsistent data, but eventually I decided that it's not necessary to fix it. In annotate-snippets model the struct gets passed to a function which generates a DisplayList our of it. It's on this step that the validation of the input (Snippet) takes place and can be rejected.

I really like this model for a foundational functionality of a foundational crate. I'm sure it's possible to build different nicer high-level APIs to aid people in constructing Snippet or Slice, but I think we should focus on exposing everything we can now and letting others or ourselves add sugar later.

Your model seems much closer to what codespan and in result language-reporting are doing. I would prefer us to avoid starting with that API, while I'm open to get to it later.

Flexibility

annotate-snippets is intentionally very vague about the core concepts in it. It is meant to be a vague API which can be used for displaying errors, but also for tutorials, helpers, explainers etc. In particular I believe that the range of annotations in them can be very vast and I'd love to end up with something flexible and extendible.
For that reason I'd like to minimize the amount of places in our API that we name after some function. Due to the nature of your proposed API you not only do that, but also add API methods like "primary", "secondary", etc. while at the same time making it a bit less visible what is the relation between them, if it's possible to specify multiple or just one (can I specify multiple "secondary()"? I can! But can I do the same to "primary()"? If so, what it means? And can I specify multiple "level()"? Or will the next one overwrite the previous one?), and so on.

For me, the difference between:

    let diagnostic = Diagnostic::build()
        .primary(
            Annotation::build()
                .span(50..777)
                .message("mismatched types")
                .build(),
        )
        .code("E0308")
        .level(Level::Err)
        .secondary(
            Annotation::build()
                .span(55..69)
                .message("expected `Option<String>` because of return type")
                .build(),
        )
        .secondary(
            Annotation::build()
                .span(76..775)
                .message("expected enum `std::option::Option`, found ()")
                .build(),
        )
        .build();

and

    let snippet = Snippet {
        title: Some(Annotation {
            id: Some("E0308"),
            label: Some("mismatched types"),
            annotation_type: AnnotationType::Error,
        }),
        slices: &[Slice {
            source,
            line_start: Some(51),
            annotations: vec![
                SourceAnnotation {
                    label: "expected `Option<String>` because of return type",
                    annotation_type: AnnotationType::Warning,
                    range: (5, 19),
                },
                SourceAnnotation {
                    label: "expected enum `std::option::Option`",
                    annotation_type: AnnotationType::Error,
                    range: (23, 725),
                },
            ],
        }],
    };

is that in the latter, the only meaning is assigned to annotations via annotation_type, and there may be many different ones including custom ones added by the user.

In the former, the title is decided by a primary and thus cannot be different than an in-source annotation, the level is defined per slice, not per annotation, and we use the concept of primary/secondary which would only be extensible via some tertiary?

annotate-snippets allows you to define title different than any source annotations, or footer, or multiple footers, multiple titles, multiple annotations, which may or may not overlap. It seems like a fairly low-level approach, but in result very flexible.

Your API seems much more constrain and intended for getting just one style of annotation snippets.

Cow

You use a lot of Cow but I'm not sure how valuable it is. I'm not as convinced to that decision, but I think that in all cases I've been able to find, &str works well, and I'm not sure if we need an owned messages by the annotation/slice/snippet.

Performance

I focus a lot on performance in my rewrite of annotate-snippets in #12 . I was unable to compile your PR so I can't measure performance but I think it'd be important to compare.

API discrepancy

Finally, and I struggle to ask this since it borderlines NIH-bias which I'd like to avoid, I'm wondering why do you feel the need to design a new API. I asked the authors of codespan and language-reporting if they see any shortcomings of my crate and they stated that they don't and the plan to converge on annotate-snippets API seems reasonable unless we find any limitation of it.
Have you encountered a limitation? Do you dislike annotation-snippets API? Any other reason?

===

I've been on vacation over this week, but I plan to get back and finish #12 now. If you believe that there's a value in diverging from its API, I'm happy to discuss the above differences (or any other that you see!) and compare the results!
I don't want to be attached to my API but I have not seen or heard of any problem with it yet, and I find it the most flexible, robust and extendible of all I've seen.
In your code I noticed several ideas which I like, but they're internal rather than API surface and I'd rather see them as PRs against annotate-snippets than a full new API.

Let me know what you think!

CAD97

CAD97 commented on Oct 12, 2019

@CAD97
Author

Big apologies: I forgot that I had an out-of-date example in the repository when I posted this; I didn't mean to mislead. I've dropped the builder API (if you look at the API overview in the OP, it's not present) and that's why the example won't compile. There were a few other issues as well because I pushed a WIP checkpoint commit. The implementation is still not quite finished for performance testing as I still need to port one final bit first.

So let me address the points some:

Dependencies

lsp-types is the only heavy dependency, and should be completely opt-in for the LSP target. The PR now correctly marks the dependency as optional. The LSP conversion could also be pulled out-of-tree if really desired, but the diagnostic layout should be LSP-friendly. Of the other three:

  • termcolor (+ wincolor, winapi-util, winapi+friends) is I think the best option for an abstracted color-capable sink (codespan/language-reportingagree with me here). @brendanzab says the current use ofansi_term(which also depends onwinapi+friends) is what currently keeps them from going all-in on annotate-snippets` (being the lack of injectable custom writer and global state).
  • scopeguard: highly used leaf crate; could be inlined if really desired.
  • bytecount: I included it because clippy yelled at me not to do a naive byte count for newline characters. It's also a leaf crate. Given expected source sizes, it might be reasonable to drop it. Is only used in impl SpanResolver for &str.

For some reason I'm having trouble installing cargo-tree so I can't give a better overview currently.

API

The builder API used in the example was old and discarded; I'm just allowing record creation much closer to annotate-snippets now:

let diagnostic = Diagnostic {
    primary: Annotation {
        span: (50, 777),
        level: Level::Err,
        message: "mismatched types".into(),
    },
    code: Some("E0308".into()),
    secondary: vec![
        Annotation {
            span: (55, 69),
            level: Level::Info,
            message: "expected `Option<String>` because of return type".into(),
        },
        Annotation {
            span: (76, 775),
            level: Level::Err,
            message: "expected enum `std::option::Option`, found ()".into(),
        },
    ] // can also be borrowed, `vec` for simplicity
    .into(),
};

Flexibility

Yeah, the API I've proposed as-is is quite targeted at diagnostics and being compatible with the LSP diagnostic API. I'm perfectly happy to generalize it some more, though.

The intent as currently designed is that the primary annotation is the "short" annotation (i.e. the one that shows up as the red squiggly before asking for more information), and the secondary annotations are any related information. Note that the secondary annotations do not need to be within the primary span or even from the same Span.origin; that's just a limitation of my current implementation trying to cobble something together to demonstrate the API.

Cow

The main reason I've used Cow is to help the use case where a consumer wants to build a Diagnostic/Annotation list up during some analysis. If they're purely borrowed, the user has to implement a similar structure that's owned to build up the list, then borrow it when pushing it to the sink annotate-snippets renderer. I'd like to avoid that necessity, but I can be talked down if it's a sticking point, especially if we provide an owned variant separate instead of mushing them together with Cow. (Basically, I'm not using Cow as copy-on-write but as maybe-owned.)

Performance

Should be on par with cleanup, as the real work was directly ported from it. Again, port is not quite finished, so can't be measured yet.

NIH

I'm happy to find a middle ground, this is mainly to share the results of my experimentation so that we can try to find the best end result. The big parts of this I'd really like to see adopted in some form: 1) A LSP target is practical, 2) delayed Span resolution to support source highlighting, and 3) a generic WriteColor target rather than baking in ANSI or no color as the only target implicitly.

matklad

matklad commented on Oct 12, 2019

@matklad
Member

I didn't read this past the first paragraph (I hope to do so, once I have more time), but I'd advise against using lsp-types as a dependency, even an optional one, It changes way to often, and I don't think it's worth it make it non-breaking. Rather, I think it's better to vendor just diagnostics related bits of the LSP, which should be stable. See, for example how I've hard-coded setup/tear down messages in lsp-server. It seems like the case where depending on a 3rd party lib doesn't actually by that much

zbraniecki

zbraniecki commented on Oct 12, 2019

@zbraniecki
Contributor

Thanks for the response!

I'm just responding to your points, I'll review more tomorrow:

Dependencies

Yes, I can understand why you aim for lsp-types. Maybe we want it, I'll have to look deeper.

As for termcolor vs ansi_term - I'm not strongly opinionated. I can see us switching if termcolor is better. I want it to be optional tho.

Others - I'd like to all extra functionality to be optional. I can see an argument for, say, unicode crate for character boundaries count and breaking, but I believe it should be optional because a fundantional crate is likely to be used often without that extra piece.

I saw things like serde and serde_json compiled as part of your PR. I think they should not be necessary.
If they're needed by lsp-types I will question whether we need lsp-types :)

API

Oh, I'm sorry for not reviewing the example vs. code. As I said, I just got back from PTO.

As for your example, it looks much better to me! I still would like to separate title from in-source annotations, because it's easier to reference/copy one from another than to separate if you need them to be different.

Flexibility

As with above - it's easier to specialize later, if the crate allows for generic behavior. I'd like to not dictate what behavior happens on the level of our crate, but rather on the level of some higher level API. Then you can have an API that uses the foundational crate and is specific to generating errors and it picks the "primary" and sets it as the only title, and as the primary annotation and so on.

Cow

I can be talked up to incorporate Cow. My main concern about it is that I'm torn on whether the API should be constructable. There's one way to think about it that it should. There's another that there should be some higher level that constructs it (maybe over time) and then spawns the Diagnostics struct.
As I said, I can definitely be talked into the idea of making the API buildable.

One idea against is that I'm trying to minimize allocations and one way to do that was to cut out all Vec replacing them with &[]. That would make the code simpler at the cost that when you need to build, you do this prior. My initial position is that in many cases you would be able to avoid that step so there's a tangible win. But maybe I'm wrong here! I'll investigate!

Performance

Cool! Let's both finish our PRs and compare! :)

NIH

Oh, awesome! I'm so happy to see actual points listed out this way. This is very helpful, thank you!

  • I'm not sure about the lsp-target, but I admit ignorance and commit to investigate.
  • I'm not sure if I understand "delayed Span resolution to support syntax highlighting". I think annotate-snippets supports syntax highlighting, and if it doesn't, it's a bug and I'm open to look for ways to fix it!
  • I am playing with the styles in the cleanup branch now. I want it to be agnostic of the styling, and able to support different stylesheets and even different themes (think - you build different DisplayList for terminal and different for web browser or some other rich GUI, which is different from just styling it).

The last step is the last piece of my cleanup, so I'd like to finish my proposal. I like your updated example much more (d'uh! it's closer to annotate-snippets ;)) and I'm really excited to see you bringing your experience and perspective! Let's finish our PRs and compare them and figure out how to approach it.
From your response I feel that we're aiming for close enough goal that we should end up with a single crate, which is awesome (the alternative is also awesome, but less attractive for the bus-factor removal which I care about deeply!).

Onwards! :)

CAD97

CAD97 commented on Oct 12, 2019

@CAD97
Author

Just a few more notes:

  • I'm convinced now that LSP support should be out-of-tree.
  • To that effect, perhaps I/we could make a diagnostics library that supports LSP that wraps annotate-snippets's more general snippet annotation.
  • The &mut dyn WriteColor sink is probably my most desired part of this proposal.
  • Second most desired is asking the SpanResolver to paint the span.

The rest is a question of what layout decisions should be at what layer.

zbraniecki

zbraniecki commented on Oct 12, 2019

@zbraniecki
Contributor

Sounds good! I'll look into &mut dyn WriteColor tomorrow, and then into SpanResolver. Give me several days and I should have something tangible (either code or opinion at least!)

kevinmehall

kevinmehall commented on Oct 12, 2019

@kevinmehall

Is the intention that you could impl Span for types like codemap::Span and codespan::Span that do the "index into concatenation of all files" trick? I don't see how they could provide origin() without reference to codemap::CodeMap / codespan::Files. Alone, these span types can't provide a filename to display, or even test whether two spans refer to the same file for the Eq bound.

The requirement that a span's start and end are usize also precludes implementing Span for types that store positions as a separate line and column number.

What if the interface exposed column numbers for the library to do its layout computations, but kept Span opaque? Here's a rough sketch:

trait SpanResolver<Sp> {
    type Origin: Eq + Display;
    type Line: Copy + Eq + Ord + Into<usize>;

    fn resolve(&mut self, span: Sp) -> ResolvedSpan<Origin, Line>;
    fn write_source(
        &mut self,
        w: &mut dyn WriteColor,
        file: &Origin,
        line: Line,
        start_col: usize,
        end_col: usize,
    ) -> io::Result<()>;
    fn next_line(&mut self, line: Line) -> Option<Line>;
}

struct ResolvedSpan<Origin, Line> {
    file: Origin,
    start_line: Line,
    start_col: usize,
    end_line: Line,
    end_col: usize,
}

Line is generic and not usize because it's O(n) to index a plain string by line number. An implementation of Line for str could cache the byte index of the start of line.

CAD97

CAD97 commented on Oct 12, 2019

@CAD97
Author

@kevinmehall yes, the intent was that it would be possible to implement Span for types that map multiple files into one dimensional space. I overlooked that resolution to the origin would have to go through the resolver as well for that to work. Of course, now with annotate-snippets being targeted lower-level than this sketch originally aimed, I don't think a single render should need to deal with spans from multiple origins at all. (That would be the next level up.)

kevinmehall

kevinmehall commented on Oct 13, 2019

@kevinmehall

I don't think a single render should need to deal with spans from multiple origins at all.

rustc has some diagnostics where the primary and secondary spans are in different files. How would that be handled? For example:

error[E0326]: implemented const `X` has an incompatible type for trait
 --> src/lib.rs:5:12
  |
5 |   const X: () = ();
  |            ^^ expected u32, found ()
  | 
 ::: src/f1.rs:2:13
  |
2 |    const X: u32;
  |             --- type in trait
  |
  = note: expected type `u32`
             found type `()`
CAD97

CAD97 commented on Oct 13, 2019

@CAD97
Author

Let me drop this here while it's fresh on my mind, though I should admit it's unbaked while I need to run off to bed again.

Here's the input structure I'm experimenting with for a much reduced annotate-snippets responsibility from the OP, now with documentation!:

API
/// A span of a snippet to be annotated.
pub trait Span {
    /// A position within the span. The only requirement is that positions
    /// sort correctly for every `Span` from the same origin.
    ///
    /// For most spans, this will be a `usize` index
    /// or a `(usize, usize)` line/column pair.
    type Pos: Ord;

    /// The start position of the span.
    ///
    /// This is expected to be equivalent in cost to an access.
    fn start(&self) -> Self::Pos;

    /// The end position of the span.
    ///
    /// This is expected to be equivalent in cost to an access.
    fn end(&self) -> Self::Pos;
}

/// A type to resolve spans from opaque spans to information required for annotation.
pub trait SpanResolver<Sp> {
    /// Write the span to a [`WriteColor`] sink.
    ///
    /// When calling `write_span`, the writer is styled with the base style.
    /// Style can be customized manually or by proxying through the stylesheet.
    fn write_span(
        &mut self,
        w: &mut dyn WriteColor,
        stylesheet: &mut dyn Stylesheet,
        span: Sp,
    ) -> io::Result<()>;

    /// Count the number of characters wide this span is in a terminal font.
    fn count_chars(&mut self, span: Sp) -> usize;

    /// Get the first line in a span. The line includes the whole line,
    /// even if that extends out of the source span being iterated.
    ///
    /// If the input span is empty, the line it is on is produced.
    fn first_line_of(&mut self, span: Sp) -> Line<Sp>;
    /// Get the next line in a span. The line includes the whole line,
    /// even if that extends out of the source span being iterated.
    ///
    /// If the next line does not overlap the span at all, `None` is produced.
    fn next_line_of(&mut self, span: Sp, previous: Line<Sp>) -> Option<Line<Sp>>;
}

/// A reference to a line within a snippet.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Line<Sp> {
    /// The span of the line, _not_ including the terminating newline (if present).
    pub span: Sp,
    /// The line number.
    pub num: usize,
}

/// One snippet to be annotated.
///
/// # Example
///
/// In the error message
///
// Please keep in sync with the `moved_value` example!
/// ```text
/// error[E0382]: use of moved value: `x`
///  --> examples/moved_value.rs:4:5
///   |
/// 4 |     let x = vec![1];
///   |         - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
/// 7 |     let y = x;
///   |             - value moved here
/// 9 |     x;
///   |     ^ value used here after move
/// ```
///
/// there are three snippets: one for each bit of code being annotated.
/// The spans to create this error are:
///
/// ```
/// # use retort::*;
/// # let line4 = 0..0; let line7 = 0..0; let line9 = 0..0;
/// let snippets = &[
///     Snippet {
///         annotated_span: line4,
///         spacing: Spacing::TightBelow,
///     },
///     Snippet {
///         annotated_span: line7,
///         spacing: Spacing::Tight,
///     },
///     Snippet {
///         annotated_span: line9,
///         spacing: Spacing::Tight,
///     },
/// ];
/// ```
#[derive(Debug, Copy, Clone)]
pub struct Snippet<Sp> {
    pub annotated_span: Sp,
    pub spacing: Spacing,
}

/// Spacing of a snippet.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Spacing {
    /// Emit a spacing line above and below the snippet.
    Spacious,
    /// Emit a spacing line below the snippet only.
    TightAbove,
    /// Emit a spacing line above the snippet only.
    TightBelow,
    /// Emit no spacing lines.
    Tight,
}

/// An annotation of some span.
///
/// # Example
///
/// In the error message
///
// Please keep in sync with the `moved_value` example!
/// ```text
/// error[E0382]: use of moved value: `x`
///  --> examples/moved_value.rs:4:5
///   |
/// 4 |     let x = vec![1];
///   |         - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
/// 7 |     let y = x;
///   |             - value moved here
/// 9 |     x;
///   |     ^ value used here after move
/// ```
///
/// there are three annotations: one on each line of code.
/// The annotations in this error are:
///
/// ```
/// # use retort::*;
/// # let line4_x = 0..0; let line7_x = 0..0; let line9_x = 0..0;
/// let annotations = &[
///     Annoatation {
///         span: line4_x,
///         message: "move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait",
///         level: Level::Information,
///     },
///     Annoatation {
///         span: line7_x,
///         message: "value moved here",
///         level: Level::Information,
///     },
///     Annoatation {
///         span: line9_x,
///         message: "value used here after move",
///         level: Level::Error,
///     },
/// ];
/// ```
#[derive(Debug, Copy, Clone)]
pub struct Annotation<'a, Sp> {
    /// The span to be annotated.
    pub span: Sp,
    /// The message to attach to the span.
    pub message: &'a str,
    /// The severity of the annotation.
    pub level: Level,
}

/// A level of severity for an annotation.
#[derive(Debug, Copy, Clone)]
pub enum Level {
    /// An error on the hand of the user.
    Error,
    /// A warning of something that isn't necessarily wrong, but looks fishy.
    Warning,
    /// An informational annotation.
    Information,
    /// A hint about what actions can be taken.
    Hint,
}

I suspect adding a "collection of snippets" type (that if I'm not mistaken, is closer to the current Snippet than mine, which is closer to Slice) would be desirable on top of this and as the argument for the render function. That "snippet collection" would also hold the front matter. Or we could even just say that we only care about snippet annotation and the caller should handle the other lines.

EDIT: recording two concerns that this API cannot cover (yet?):

  • Alignment of notes not attached to annotation with the separator
  • Alignment of the separator between annotated slices.
Marwes

Marwes commented on Oct 13, 2019

@Marwes

@matklad

I didn't read this past the first paragraph (I hope to do so, once I have more time), but I'd advise against using lsp-types as a dependency, even an optional one, It changes way to often, and I don't think it's worth it make it non-breaking.

Did you see gluon-lang/lsp-types#117 ? Would let lsp-types go to 1.0 at the cost of having a slightly more awkward API (though in a way, a more honest one).

brendanzab

brendanzab commented on Oct 14, 2019

@brendanzab
Member

I just want to say I really appreciate the time being put into this! Would be exciting to converge on something nice.

24 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-enhancementCategory: enhancementE-help-wantedCall for participation: Help is requested to fix this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @wycats@epage@kevinmehall@zbraniecki@brendanzab

        Issue actions

          RFC: Proposal for "modern" API · Issue #13 · rust-lang/annotate-snippets-rs