@@ -47,6 +47,22 @@ impl Label {
4747 self . color = color;
4848 self
4949 }
50+
51+ /// Borrow the secondary label's message without copying.
52+ ///
53+ /// Surfaced for `Formatted::diagnostic_view`, which lowers labels into
54+ /// LSP-facing related-information entries without re-parsing rendered text.
55+ pub fn message ( & self ) -> & str {
56+ & self . msg
57+ }
58+
59+ /// Return the secondary label's source span.
60+ ///
61+ /// Used by structured diagnostic consumers (notably `leo-lsp`) to convert
62+ /// labels into UTF-16 ranges while the session source map is still alive.
63+ pub fn span ( & self ) -> Span {
64+ self . span
65+ }
5066}
5167
5268/// Helper span for Ariadne that includes the source file start index.
@@ -173,6 +189,83 @@ impl Formatted {
173189 format_warning_code ( & self . inner . type_ , 37 , self . inner . code )
174190 }
175191
192+ /// Return the diagnostic's primary message without ariadne rendering.
193+ ///
194+ /// Used by tooling consumers (notably `leo-lsp`) that need the bare message
195+ /// text rather than the formatted report. The returned slice borrows from
196+ /// the same allocation as the rest of the diagnostic and therefore stays
197+ /// valid for the lifetime of the `Formatted` value.
198+ pub fn message ( & self ) -> & str {
199+ & self . inner . message
200+ }
201+
202+ /// Return the diagnostic's optional help text, if any.
203+ ///
204+ /// `leo-lsp` appends this to the LSP diagnostic message so editor clients
205+ /// see the same hint that the CLI report would render.
206+ pub fn help ( & self ) -> Option < & str > {
207+ self . inner . help . as_deref ( )
208+ }
209+
210+ /// Return the diagnostic's optional follow-up note text, if any.
211+ ///
212+ /// `leo-lsp` appends this to the LSP diagnostic message for parity with
213+ /// CLI-rendered reports.
214+ pub fn note ( & self ) -> Option < & str > {
215+ self . inner . note . as_deref ( )
216+ }
217+
218+ /// Return whether this diagnostic was raised as an error rather than a
219+ /// warning.
220+ ///
221+ /// LSP severity mapping depends on this flag instead of inspecting the
222+ /// rendered `Error`/`Warning` prefix in the formatted message.
223+ pub fn is_error ( & self ) -> bool {
224+ self . inner . error
225+ }
226+
227+ /// Return the diagnostic's primary span.
228+ ///
229+ /// Callers must resolve this span against `leo_span` session globals to
230+ /// recover the originating source file before the surrounding session is
231+ /// torn down.
232+ pub fn span ( & self ) -> Span {
233+ self . inner . span
234+ }
235+
236+ /// Iterate the diagnostic's secondary labels in declaration order.
237+ ///
238+ /// Labels carry their own span and human-readable message, which `leo-lsp`
239+ /// surfaces as `Diagnostic.relatedInformation` when the client supports it.
240+ pub fn labels ( & self ) -> impl Iterator < Item = & Label > {
241+ self . inner . labels . iter ( )
242+ }
243+
244+ /// Borrow this diagnostic as a plain structured view.
245+ ///
246+ /// The returned [`DiagnosticView`] exposes the same fields that are used
247+ /// when rendering the ariadne report, without round-tripping through a
248+ /// formatted string. Consumers like `leo-lsp` use the view to build LSP
249+ /// `Diagnostic` payloads without parsing rendered output.
250+ pub fn diagnostic_view ( & self ) -> DiagnosticView < ' _ > {
251+ let code = if self . inner . error { self . error_code ( ) } else { self . warning_code ( ) } ;
252+ let labels = self
253+ . inner
254+ . labels
255+ . iter ( )
256+ . map ( |label| DiagnosticLabelView { message : label. message ( ) . to_owned ( ) , span : label. span ( ) } )
257+ . collect ( ) ;
258+ DiagnosticView {
259+ message : & self . inner . message ,
260+ help : self . inner . help . as_deref ( ) ,
261+ note : self . inner . note . as_deref ( ) ,
262+ code,
263+ is_error : self . inner . error ,
264+ span : Some ( self . inner . span ) ,
265+ labels,
266+ }
267+ }
268+
176269 /// Resolve a Leo `Span` to an `AriadneSpan` using the source map.
177270 fn resolve_span ( span : Span , source_map : & leo_span:: source_map:: SourceMap ) -> AriadneSpan {
178271 let file_start_index = source_map. find_source_file ( span. lo ) . unwrap ( ) . absolute_start ;
@@ -265,3 +358,91 @@ impl std::error::Error for Formatted {
265358 & self . inner . message
266359 }
267360}
361+
362+ /// LSP-agnostic structured view of a compiler diagnostic.
363+ ///
364+ /// Exposed so editor tooling — currently `leo-lsp` — can lower errors and
365+ /// warnings into editor-facing diagnostics without parsing rendered ariadne
366+ /// output. The view borrows from the originating [`Formatted`] for cheap
367+ /// strings while owning a small per-label `Vec`, which is the smallest shape
368+ /// that keeps secondary-label messages alive across an `extract_errs` call.
369+ #[ derive( Debug , Clone ) ]
370+ pub struct DiagnosticView < ' a > {
371+ /// Primary human-readable message.
372+ pub message : & ' a str ,
373+ /// Optional help hint shown beneath the diagnostic on the CLI.
374+ pub help : Option < & ' a str > ,
375+ /// Optional follow-up note shown beneath the help line on the CLI.
376+ pub note : Option < & ' a str > ,
377+ /// Fully formatted code identifier (e.g. `EPAR0001` or `WTYC0001`).
378+ pub code : String ,
379+ /// Whether the diagnostic is an error (`true`) or a warning (`false`).
380+ pub is_error : bool ,
381+ /// Primary span, when the diagnostic ties to a concrete source location.
382+ pub span : Option < Span > ,
383+ /// Secondary spans annotated with their own human-readable messages.
384+ pub labels : Vec < DiagnosticLabelView > ,
385+ }
386+
387+ /// One secondary label paired with its source span.
388+ ///
389+ /// `leo-lsp` lowers each label into `Diagnostic.relatedInformation` when the
390+ /// client advertises support, so it captures both the span and the message.
391+ #[ derive( Debug , Clone ) ]
392+ pub struct DiagnosticLabelView {
393+ /// Human-readable description for the label.
394+ pub message : String ,
395+ /// Span associated with the label, resolved against session source globals.
396+ pub span : Span ,
397+ }
398+
399+ #[ cfg( test) ]
400+ mod tests {
401+ use super :: { Color , Formatted , Label } ;
402+ use leo_span:: { Span , create_session_if_not_set_then} ;
403+
404+ /// Verifies the structured view round-trips primary message, code, help, and note.
405+ #[ test]
406+ fn diagnostic_view_exposes_primary_fields ( ) {
407+ create_session_if_not_set_then ( |_| {
408+ let span = Span :: default ( ) ;
409+ let error = Formatted :: error ( "TST" , 1 , "boom" , span) . with_help ( "try again" ) . with_note ( "note text" ) ;
410+
411+ let view = error. diagnostic_view ( ) ;
412+ assert_eq ! ( view. message, "boom" ) ;
413+ assert_eq ! ( view. help, Some ( "try again" ) ) ;
414+ assert_eq ! ( view. note, Some ( "note text" ) ) ;
415+ assert_eq ! ( view. code, error. error_code( ) ) ;
416+ assert ! ( view. is_error) ;
417+ assert_eq ! ( view. span, Some ( span) ) ;
418+ assert ! ( view. labels. is_empty( ) ) ;
419+ } ) ;
420+ }
421+
422+ /// Verifies labels are exposed with their messages and spans intact.
423+ #[ test]
424+ fn diagnostic_view_exposes_secondary_labels ( ) {
425+ create_session_if_not_set_then ( |_| {
426+ let primary = Span :: new ( 0 , 4 ) ;
427+ let label_span = Span :: new ( 5 , 10 ) ;
428+ let error = Formatted :: error ( "TST" , 2 , "boom" , primary)
429+ . with_label ( Label :: new ( label_span) . with_message ( "see also" ) . with_color ( Color :: Blue ) ) ;
430+
431+ let view = error. diagnostic_view ( ) ;
432+ assert_eq ! ( view. labels. len( ) , 1 ) ;
433+ assert_eq ! ( view. labels[ 0 ] . message, "see also" ) ;
434+ assert_eq ! ( view. labels[ 0 ] . span, label_span) ;
435+ } ) ;
436+ }
437+
438+ /// Verifies warnings round-trip through the structured view with severity preserved.
439+ #[ test]
440+ fn diagnostic_view_marks_warnings ( ) {
441+ create_session_if_not_set_then ( |_| {
442+ let warning = Formatted :: warning ( "TST" , 3 , "watch out" , Span :: default ( ) ) ;
443+ let view = warning. diagnostic_view ( ) ;
444+ assert ! ( !view. is_error) ;
445+ assert_eq ! ( view. code, warning. warning_code( ) ) ;
446+ } ) ;
447+ }
448+ }
0 commit comments