1212
1313import SwiftSyntax
1414
15+ extension Sequence where Element == Range < Int > {
16+ /// Given a set of ranges that are sorted in order of nondecreasing lower
17+ /// bound, merge any overlapping ranges to produce a sequence of
18+ /// nonoverlapping ranges.
19+ fileprivate func mergingOverlappingRanges( ) -> [ Range < Int > ] {
20+ var result : [ Range < Int > ] = [ ]
21+
22+ var prior : Range < Int > ? = nil
23+ for range in self {
24+ // If this is the first range we've seen, note it as the prior and
25+ // continue.
26+ guard let priorRange = prior else {
27+ prior = range
28+ continue
29+ }
30+
31+ // If the ranges overlap, expand the prior range.
32+ if priorRange. overlaps ( range) {
33+ let lower = Swift . min ( priorRange. lowerBound, range. lowerBound)
34+ let upper = Swift . max ( priorRange. upperBound, range. upperBound)
35+ prior = lower..< upper
36+ continue
37+ }
38+
39+ // Append the prior range, then take this new range as the prior
40+ result. append ( priorRange)
41+ prior = range
42+ }
43+
44+ if let priorRange = prior {
45+ result. append ( priorRange)
46+ }
47+ return result
48+ }
49+ }
50+
1551public struct DiagnosticsFormatter {
1652
17- /// A wrapper struct for a source line and its diagnostics
53+ /// A wrapper struct for a source line, its diagnostics, and any
54+ /// non-diagnostic text that follows the line.
1855 private struct AnnotatedSourceLine {
1956 var diagnostics : [ Diagnostic ]
2057 var sourceString : String
58+
59+ /// Non-diagnostic text that is appended after this source line.
60+ ///
61+ /// Suffix text can be used to provide more information following a source
62+ /// line, such as to provide an inset source buffer for a macro expansion
63+ /// that occurs on that line.
64+ var suffixText : String
65+
66+ /// Whether this line is free of annotations.
67+ var isFreeOfAnnotations : Bool {
68+ return diagnostics. isEmpty && suffixText. isEmpty
69+ }
2170 }
2271
2372 /// Number of lines which should be printed before and after the diagnostic message
@@ -41,9 +90,108 @@ public struct DiagnosticsFormatter {
4190 return formatter. annotatedSource ( tree: tree, diags: diags)
4291 }
4392
93+ /// Colorize the given source line by applying highlights from diagnostics.
94+ private func colorizeSourceLine< SyntaxType: SyntaxProtocol > (
95+ _ annotatedLine: AnnotatedSourceLine ,
96+ lineNumber: Int ,
97+ tree: SyntaxType ,
98+ sourceLocationConverter slc: SourceLocationConverter
99+ ) -> String {
100+ guard colorize, !annotatedLine. diagnostics. isEmpty else {
101+ return annotatedLine. sourceString
102+ }
103+
104+ // Compute the set of highlight ranges that land on this line. These
105+ // are column ranges, sorted in order of increasing starting column, and
106+ // with overlapping ranges merged.
107+ let highlightRanges : [ Range < Int > ] = annotatedLine. diagnostics. map {
108+ $0. highlights
109+ } . joined ( ) . compactMap { ( highlight) -> Range < Int > ? in
110+ if highlight. root != Syntax ( tree) {
111+ return nil
112+ }
113+
114+ let startLoc = highlight. startLocation ( converter: slc, afterLeadingTrivia: true ) ;
115+ guard let startLine = startLoc. line else {
116+ return nil
117+ }
118+
119+ // Find the starting column.
120+ let startColumn : Int
121+ if startLine < lineNumber {
122+ startColumn = 1
123+ } else if startLine == lineNumber, let column = startLoc. column {
124+ startColumn = column
125+ } else {
126+ return nil
127+ }
128+
129+ // Find the ending column.
130+ let endLoc = highlight. endLocation ( converter: slc, afterTrailingTrivia: false )
131+ guard let endLine = endLoc. line else {
132+ return nil
133+ }
134+
135+ let endColumn : Int
136+ if endLine > lineNumber {
137+ endColumn = annotatedLine. sourceString. count
138+ } else if endLine == lineNumber, let column = endLoc. column {
139+ endColumn = column
140+ } else {
141+ return nil
142+ }
143+
144+ if startColumn == endColumn {
145+ return nil
146+ }
147+
148+ return startColumn..< endColumn
149+ } . sorted { ( lhs, rhs) in
150+ lhs. lowerBound < rhs. lowerBound
151+ } . mergingOverlappingRanges ( )
152+
153+ // Map the column ranges into index ranges within the source string itself.
154+ let sourceString = annotatedLine. sourceString
155+ let highlightIndexRanges : [ Range < String . Index > ] = highlightRanges. map { highlightRange in
156+ let startIndex = sourceString. index ( sourceString. startIndex, offsetBy: highlightRange. lowerBound - 1 )
157+ let endIndex = sourceString. index ( startIndex, offsetBy: highlightRange. count)
158+ return startIndex..< endIndex
159+ }
160+
161+ // Form the annotated string by copying in text from the original source,
162+ // highlighting the column ranges.
163+ var resultSourceString : String = " "
164+ var sourceIndex = sourceString. startIndex
165+ let annotation = ANSIAnnotation . sourceHighlight
166+ for highlightRange in highlightIndexRanges {
167+ // Text before the highlight range
168+ resultSourceString += sourceString [ sourceIndex..< highlightRange. lowerBound]
169+
170+ // Highlighted source text
171+ let highlightString = String ( sourceString [ highlightRange] )
172+ resultSourceString += annotation. applied ( to: highlightString)
173+
174+ sourceIndex = highlightRange. upperBound
175+ }
176+
177+ resultSourceString += sourceString [ sourceIndex... ]
178+ return resultSourceString
179+ }
180+
44181 /// Print given diagnostics for a given syntax tree on the command line
45- public func annotatedSource< SyntaxType: SyntaxProtocol > ( tree: SyntaxType , diags: [ Diagnostic ] ) -> String {
46- let slc = SourceLocationConverter ( file: " " , tree: tree)
182+ ///
183+ /// - Parameters:
184+ /// - suffixTexts: suffix text to be printed at the given absolute
185+ /// locations within the source file.
186+ func annotatedSource< SyntaxType: SyntaxProtocol > (
187+ fileName: String ? ,
188+ tree: SyntaxType ,
189+ diags: [ Diagnostic ] ,
190+ indentString: String ,
191+ suffixTexts: [ ( AbsolutePosition , String ) ] ,
192+ sourceLocationConverter: SourceLocationConverter ? = nil
193+ ) -> String {
194+ let slc = sourceLocationConverter ?? SourceLocationConverter ( file: fileName ?? " " , tree: tree)
47195
48196 // First, we need to put each line and its diagnostics together
49197 var annotatedSourceLines = [ AnnotatedSourceLine] ( )
@@ -52,20 +200,34 @@ public struct DiagnosticsFormatter {
52200 let diagsForLine = diags. filter { diag in
53201 return diag. location ( converter: slc) . line == ( sourceLineIndex + 1 )
54202 }
55- annotatedSourceLines. append ( AnnotatedSourceLine ( diagnostics: diagsForLine, sourceString: sourceLine) )
203+ let suffixText = suffixTexts. compactMap { ( position, text) in
204+ if slc. location ( for: position) . line == ( sourceLineIndex + 1 ) {
205+ return text
206+ }
207+
208+ return nil
209+ } . joined ( )
210+
211+ annotatedSourceLines. append ( AnnotatedSourceLine ( diagnostics: diagsForLine, sourceString: sourceLine, suffixText: suffixText) )
56212 }
57213
58214 // Only lines with diagnostic messages should be printed, but including some context
59215 let rangesToPrint = annotatedSourceLines. enumerated ( ) . compactMap { ( lineIndex, sourceLine) -> Range < Int > ? in
60216 let lineNumber = lineIndex + 1
61- if !sourceLine. diagnostics . isEmpty {
217+ if !sourceLine. isFreeOfAnnotations {
62218 return Range < Int > ( uncheckedBounds: ( lower: lineNumber - contextSize, upper: lineNumber + contextSize + 1 ) )
63219 }
64220 return nil
65221 }
66222
67223 var annotatedSource = " "
68224
225+ // If there was a filename, add it first.
226+ if let fileName = fileName {
227+ let header = colorizeBufferOutline ( " === " )
228+ annotatedSource. append ( " \( indentString) \( header) \( fileName) \( header) \n " )
229+ }
230+
69231 /// Keep track if a line missing char should be printed
70232 var hasLineBeenSkipped = false
71233
@@ -85,17 +247,28 @@ public struct DiagnosticsFormatter {
85247 // line numbers should be right aligned
86248 let lineNumberString = String ( lineNumber)
87249 let leadingSpaces = String ( repeating: " " , count: maxNumberOfDigits - lineNumberString. count)
88- let linePrefix = " \( leadingSpaces) \( lineNumberString) │ "
250+ let linePrefix = " \( leadingSpaces) \( colorizeBufferOutline ( " \( lineNumberString) │ " ) ) "
89251
90252 // If necessary, print a line that indicates that there was lines skipped in the source code
91253 if hasLineBeenSkipped && !annotatedSource. isEmpty {
92- let lineMissingInfoLine = String ( repeating: " " , count: maxNumberOfDigits) + " ┆ "
254+ let lineMissingInfoLine = indentString + String( repeating: " " , count: maxNumberOfDigits) + " \( colorizeBufferOutline ( " ┆ " ) ) "
93255 annotatedSource. append ( " \( lineMissingInfoLine) \n " )
94256 }
95257 hasLineBeenSkipped = false
96258
259+ // add indentation
260+ annotatedSource. append ( indentString)
261+
97262 // print the source line
98- annotatedSource. append ( " \( linePrefix) \( annotatedLine. sourceString) " )
263+ annotatedSource. append ( linePrefix)
264+ annotatedSource. append (
265+ colorizeSourceLine (
266+ annotatedLine,
267+ lineNumber: lineNumber,
268+ tree: tree,
269+ sourceLocationConverter: slc
270+ )
271+ )
99272
100273 // If the line did not end with \n (e.g. the last line), append it manually
101274 if annotatedSource. last != " \n " {
@@ -111,7 +284,7 @@ public struct DiagnosticsFormatter {
111284
112285 for (column, diags) in diagsPerColumn {
113286 // compute the string that is shown before each message
114- var preMessage = String ( repeating: " " , count: maxNumberOfDigits) + " ∣ "
287+ var preMessage = indentString + String( repeating: " " , count: maxNumberOfDigits) + " " + colorizeBufferOutline ( " ∣ " )
115288 for c in 0 ..< column {
116289 if columnsWithDiagnostics. contains ( c) {
117290 preMessage. append ( " │ " )
@@ -125,10 +298,30 @@ public struct DiagnosticsFormatter {
125298 }
126299 annotatedSource. append ( " \( preMessage) ╰─ \( colorizeIfRequested ( diags. last!. diagMessage) ) \n " )
127300 }
301+
302+ // Add suffix text.
303+ annotatedSource. append ( annotatedLine. suffixText)
304+ if annotatedSource. last != " \n " {
305+ annotatedSource. append ( " \n " )
306+ }
128307 }
129308 return annotatedSource
130309 }
131310
311+ /// Print given diagnostics for a given syntax tree on the command line
312+ public func annotatedSource< SyntaxType: SyntaxProtocol > (
313+ tree: SyntaxType ,
314+ diags: [ Diagnostic ]
315+ ) -> String {
316+ return annotatedSource (
317+ fileName: nil ,
318+ tree: tree,
319+ diags: diags,
320+ indentString: " " ,
321+ suffixTexts: [ ]
322+ )
323+ }
324+
132325 /// Annotates the given ``DiagnosticMessage`` with an appropriate ANSI color code (if the value of the `colorize`
133326 /// property is `true`) and returns the result as a printable string.
134327 private func colorizeIfRequested( _ message: DiagnosticMessage ) -> String {
@@ -148,6 +341,24 @@ public struct DiagnosticsFormatter {
148341 return message. message
149342 }
150343 }
344+
345+ /// Apply the given color and trait to the specified text, when we are
346+ /// supposed to color the output.
347+ private func colorizeIfRequested(
348+ _ text: String ,
349+ annotation: ANSIAnnotation
350+ ) -> String {
351+ guard colorize, !text. isEmpty else {
352+ return text
353+ }
354+
355+ return annotation. applied ( to: text)
356+ }
357+
358+ /// Colorize for the buffer outline and line numbers.
359+ func colorizeBufferOutline( _ text: String ) -> String {
360+ colorizeIfRequested ( text, annotation: . bufferOutline)
361+ }
151362}
152363
153364struct ANSIAnnotation {
@@ -195,4 +406,14 @@ struct ANSIAnnotation {
195406 static var normal : ANSIAnnotation {
196407 self . init ( color: . normal, trait: . normal)
197408 }
409+
410+ /// Annotation used for the outline and line numbers of a buffer.
411+ static var bufferOutline : ANSIAnnotation {
412+ ANSIAnnotation ( color: . cyan, trait: . normal)
413+ }
414+
415+ /// Annotation used for highlighting source text.
416+ static var sourceHighlight : ANSIAnnotation {
417+ ANSIAnnotation ( color: . white, trait: . underline)
418+ }
198419}
0 commit comments