12
12
13
13
import SwiftSyntax
14
14
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
+
15
51
public struct DiagnosticsFormatter {
16
52
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.
18
55
private struct AnnotatedSourceLine {
19
56
var diagnostics : [ Diagnostic ]
20
57
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
+ }
21
70
}
22
71
23
72
/// Number of lines which should be printed before and after the diagnostic message
@@ -41,9 +90,108 @@ public struct DiagnosticsFormatter {
41
90
return formatter. annotatedSource ( tree: tree, diags: diags)
42
91
}
43
92
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
+
44
181
/// 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)
47
195
48
196
// First, we need to put each line and its diagnostics together
49
197
var annotatedSourceLines = [ AnnotatedSourceLine] ( )
@@ -52,20 +200,34 @@ public struct DiagnosticsFormatter {
52
200
let diagsForLine = diags. filter { diag in
53
201
return diag. location ( converter: slc) . line == ( sourceLineIndex + 1 )
54
202
}
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) )
56
212
}
57
213
58
214
// Only lines with diagnostic messages should be printed, but including some context
59
215
let rangesToPrint = annotatedSourceLines. enumerated ( ) . compactMap { ( lineIndex, sourceLine) -> Range < Int > ? in
60
216
let lineNumber = lineIndex + 1
61
- if !sourceLine. diagnostics . isEmpty {
217
+ if !sourceLine. isFreeOfAnnotations {
62
218
return Range < Int > ( uncheckedBounds: ( lower: lineNumber - contextSize, upper: lineNumber + contextSize + 1 ) )
63
219
}
64
220
return nil
65
221
}
66
222
67
223
var annotatedSource = " "
68
224
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
+
69
231
/// Keep track if a line missing char should be printed
70
232
var hasLineBeenSkipped = false
71
233
@@ -85,17 +247,28 @@ public struct DiagnosticsFormatter {
85
247
// line numbers should be right aligned
86
248
let lineNumberString = String ( lineNumber)
87
249
let leadingSpaces = String ( repeating: " " , count: maxNumberOfDigits - lineNumberString. count)
88
- let linePrefix = " \( leadingSpaces) \( lineNumberString) │ "
250
+ let linePrefix = " \( leadingSpaces) \( colorizeBufferOutline ( " \( lineNumberString) │ " ) ) "
89
251
90
252
// If necessary, print a line that indicates that there was lines skipped in the source code
91
253
if hasLineBeenSkipped && !annotatedSource. isEmpty {
92
- let lineMissingInfoLine = String ( repeating: " " , count: maxNumberOfDigits) + " ┆ "
254
+ let lineMissingInfoLine = indentString + String( repeating: " " , count: maxNumberOfDigits) + " \( colorizeBufferOutline ( " ┆ " ) ) "
93
255
annotatedSource. append ( " \( lineMissingInfoLine) \n " )
94
256
}
95
257
hasLineBeenSkipped = false
96
258
259
+ // add indentation
260
+ annotatedSource. append ( indentString)
261
+
97
262
// 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
+ )
99
272
100
273
// If the line did not end with \n (e.g. the last line), append it manually
101
274
if annotatedSource. last != " \n " {
@@ -111,7 +284,7 @@ public struct DiagnosticsFormatter {
111
284
112
285
for (column, diags) in diagsPerColumn {
113
286
// 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 ( " ∣ " )
115
288
for c in 0 ..< column {
116
289
if columnsWithDiagnostics. contains ( c) {
117
290
preMessage. append ( " │ " )
@@ -125,10 +298,30 @@ public struct DiagnosticsFormatter {
125
298
}
126
299
annotatedSource. append ( " \( preMessage) ╰─ \( colorizeIfRequested ( diags. last!. diagMessage) ) \n " )
127
300
}
301
+
302
+ // Add suffix text.
303
+ annotatedSource. append ( annotatedLine. suffixText)
304
+ if annotatedSource. last != " \n " {
305
+ annotatedSource. append ( " \n " )
306
+ }
128
307
}
129
308
return annotatedSource
130
309
}
131
310
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
+
132
325
/// Annotates the given ``DiagnosticMessage`` with an appropriate ANSI color code (if the value of the `colorize`
133
326
/// property is `true`) and returns the result as a printable string.
134
327
private func colorizeIfRequested( _ message: DiagnosticMessage ) -> String {
@@ -148,6 +341,24 @@ public struct DiagnosticsFormatter {
148
341
return message. message
149
342
}
150
343
}
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
+ }
151
362
}
152
363
153
364
struct ANSIAnnotation {
@@ -195,4 +406,14 @@ struct ANSIAnnotation {
195
406
static var normal : ANSIAnnotation {
196
407
self . init ( color: . normal, trait: . normal)
197
408
}
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
+ }
198
419
}
0 commit comments