Skip to content

Commit 2771bfb

Browse files
committed
Add ANSIDiagnosticDecorator and DiagnosticDecorator protocol
This commit introduces two significant components: 1. `DiagnosticDecorator` Protocol: - Defines a standard interface for decorating diagnostic output in source code. - Design to be used by entities like `DiagnosticsFormatter` and `GroupedDiagnostics`. 2. `ANSIDiagnosticDecorator` Struct: - Implements the `DiagnosticDecorator` protocol. - Adds severity-based prefixes to diagnostic messages. - Supports optional ANSI colorization to enhance visibility and interpretation. Also includes: - Unit tests for `ANSIDiagnosticDecorator` in `ANSIDiagnosticDecoratorTests`. This feature enriches the diagnostic capabilities, offering customizable and visually informative feedback.
1 parent 3bfa8bb commit 2771bfb

File tree

4 files changed

+448
-82
lines changed

4 files changed

+448
-82
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension DiagnosticDecorator where Self == ANSIDiagnosticDecorator {
14+
public static func ANSI(colorize: Bool) -> Self {
15+
Self(colorize: colorize)
16+
}
17+
}
18+
19+
/// An implementation of ``DiagnosticDecorator`` protocol designed to enhance diagnostic messages by adding severity-based
20+
/// prefixes and optional ANSI colorization.
21+
///
22+
/// By leveraging ANSI codes—a set of control characters used to manipulate the formatting
23+
/// and appearance of text in a terminal—the decorator imbues textual information with visual cues.
24+
/// These visual cues vary depending on the severity of the message, such as whether it's an error, a warning,
25+
/// or another type of diagnostic output.
26+
///
27+
/// In essence, this decorator serves two primary functions:
28+
/// 1. It appends a categorization prefix (e.g., "error: ", "warning: ", "note: ", "remark: ") to the diagnostic message
29+
/// to indicate its level of urgency or importance.
30+
/// 2. It has the capability to colorize this prefix and other segments of the message for enhanced visibility
31+
/// and quick interpretation. This colorization feature can be toggled on or off, offering flexibility in its usage.
32+
public struct ANSIDiagnosticDecorator: DiagnosticDecorator {
33+
/// A flag indicating whether or not to apply colorization.
34+
private let colorize: Bool
35+
36+
/// Initializes a new `ANSIDiagnosticDecorator` instance.
37+
///
38+
/// - Parameter colorize: A boolean flag indicating whether to apply colorization. Defaults to true.
39+
public init(colorize: Bool) {
40+
self.colorize = colorize
41+
}
42+
43+
/// Decorates a diagnostic message by adding a severity-based prefix and optionally applying ANSI colorization.
44+
///
45+
/// This method always prepends a severity-specific prefix (e.g., "error: ") to the diagnostic message. The `colorize` property
46+
/// controls whether or not ANSI color codes are applied to the prefix.
47+
///
48+
/// - Parameters:
49+
/// - message: The diagnostic message that needs to be decorated.
50+
/// - severity: The severity level associated with the diagnostic message.
51+
///
52+
/// - Returns: A string that combines the severity-specific prefix and the original diagnostic message, with optional ANSI colorization.
53+
///
54+
/// ## Examples
55+
///
56+
/// When `colorize` is set to true:
57+
/// ```swift
58+
/// let decorator = ANSIDiagnosticDecorator(colorize: true)
59+
/// let decoratedMessage = decorator.decorateMessage("File not found", basedOnSeverity: .error)
60+
/// // Output would be: "error: File not found"
61+
/// ```
62+
/// In this case, the prefix "error: " would be colorized, likely appearing in red.
63+
///
64+
/// To achieve a similar colorized output manually in the console, you can use `printf` in bash:
65+
/// ```bash
66+
/// printf "\e[1;31merror: \e[1;39mFile not found\e[0;0m\n"
67+
/// ```
68+
/// This will print the "error: " prefix in red and the message "File not found" in the default text color.
69+
///
70+
/// When `colorize` is set to false:
71+
/// ```swift
72+
/// let decorator = ANSIDiagnosticDecorator(colorize: false)
73+
/// let decoratedMessage = decorator.decorateMessage("File not found", basedOnSeverity: .error)
74+
/// // Output would be: "error: File not found"
75+
/// ```
76+
/// In this case, the prefix "error: " would appear as plain text, with no ANSI colorization applied.
77+
public func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String {
78+
let severityText: String
79+
let severityAnnotation: ANSIAnnotation
80+
81+
switch severity {
82+
case .error:
83+
severityText = "error"
84+
severityAnnotation = .errorText
85+
86+
case .warning:
87+
severityText = "warning"
88+
severityAnnotation = .warningText
89+
90+
case .note:
91+
severityText = "note"
92+
severityAnnotation = .noteText
93+
94+
case .remark:
95+
severityText = "remark"
96+
severityAnnotation = .remarkText
97+
}
98+
99+
let prefix = colorizeIfRequested("\(severityText): ", usingAnnotation: severityAnnotation, resetAfterApplication: false)
100+
101+
return prefix + colorizeIfRequested(message, usingAnnotation: .diagnosticText)
102+
}
103+
104+
/// Decorates the outline of a source code buffer, optionally applying ANSI cyan color colorization.
105+
///
106+
/// - Parameter bufferOutline: The string representation of the source code buffer outline.
107+
///
108+
/// - Returns: A string with optional ANSI cyan color colorization applied to the source code buffer outline.
109+
public func decorateBufferOutline(_ bufferOutline: String) -> String {
110+
colorizeIfRequested(bufferOutline, usingAnnotation: .bufferOutline)
111+
}
112+
113+
/// Decorates a text segment intended for highlighting within a source code snippet. The method can also optionally
114+
/// apply ANSI colorization for enhanced visual cues.
115+
///
116+
/// - Parameter highlight: The text segment to be emphasized.
117+
///
118+
/// - Returns: A tuple containing:
119+
/// - `highlightedSourceCode`: A string that represents the decorated version of the original source code snippet.
120+
/// - `additionalHighlightedLine`: always with nil.
121+
///
122+
/// ## Examples
123+
///
124+
/// When `colorize` is set to true:
125+
/// ```swift
126+
/// let decorator = ANSIDiagnosticDecorator(colorize: true)
127+
/// let decoratedHighlight = decorator.decorateHighlight("let x = 10")
128+
/// // Output would be: ["\u{1B}[4;39mlet x = 10\u{1B}[0;0m"]
129+
/// ```
130+
///
131+
/// To achieve a similar colorized output manually in the console, you can use `printf` in bash:
132+
/// ```bash
133+
/// printf "\e[4;39mlet x = 10\e[0;0m\n"
134+
/// ```
135+
///
136+
/// When `colorize` is set to false:
137+
/// ```swift
138+
/// let decorator = ANSIDiagnosticDecorator(colorize: false)
139+
/// let decoratedHighlight = decorator.decorateHighlight("let x = 10")
140+
/// // Output would be: ["let x = 10"]
141+
/// ```
142+
public func decorateHighlight(_ highlight: String) -> (highlightedSourceCode: String, additionalHighlightedLine: String?) {
143+
(highlightedSourceCode: colorizeIfRequested(highlight, usingAnnotation: .sourceHighlight), additionalHighlightedLine: nil)
144+
}
145+
146+
/// Applies ANSI annotation to a given text segment, if colorization is enabled.
147+
///
148+
/// - Parameters:
149+
/// - text: The text segment to which the annotation should be applied.
150+
/// - annotation: The ANSI annotation to apply.
151+
/// - resetAfter: A flag indicating whether to reset ANSI settings after applying them. Defaults to true.
152+
///
153+
/// - Returns: A potentially colorized version of the input text.
154+
private func colorizeIfRequested(
155+
_ text: String,
156+
usingAnnotation annotation: ANSIAnnotation,
157+
resetAfterApplication resetAfter: Bool = true
158+
) -> String {
159+
guard colorize, !text.isEmpty else {
160+
return text
161+
}
162+
163+
return annotation.applied(to: text, resetAfter: resetAfter)
164+
}
165+
}
166+
167+
/// Defines text attributes to be applied to console output.
168+
struct ANSIAnnotation {
169+
/// Represents ANSI color codes.
170+
enum Color: UInt8 {
171+
case normal = 0
172+
case black = 30
173+
case red = 31
174+
case green = 32
175+
case yellow = 33
176+
case blue = 34
177+
case magenta = 35
178+
case cyan = 36
179+
case white = 37
180+
case `default` = 39
181+
}
182+
183+
/// Represents ANSI text traits.
184+
enum Trait: UInt8 {
185+
case normal = 0
186+
case bold = 1
187+
case underline = 4
188+
}
189+
190+
/// The ANSI color to be used.
191+
let color: Color
192+
193+
/// The ANSI text trait to be used.
194+
let trait: Trait
195+
196+
/// Returns ANSI code as a string, including both trait and color.
197+
var code: String {
198+
"\u{001B}[\(trait.rawValue);\(color.rawValue)m"
199+
}
200+
201+
/// Applies the ANSI code to a message string. Optionally resets the code after the message.
202+
func applied(to message: String, resetAfter: Bool = true) -> String {
203+
guard resetAfter else {
204+
return "\(code)\(message)"
205+
}
206+
return "\(code)\(message)\(ANSIAnnotation.normal.code)"
207+
}
208+
209+
/// The default 'normal' ANSIAnnotation used to reset styles.
210+
static var normal: Self {
211+
Self(color: .normal, trait: .normal)
212+
}
213+
214+
/// Annotation used for the outline and line numbers of a buffer.
215+
static var bufferOutline: Self {
216+
Self(color: .cyan, trait: .normal)
217+
}
218+
219+
/// Annotation used for highlighting source text.
220+
static var sourceHighlight: Self {
221+
Self(color: .default, trait: .underline)
222+
}
223+
224+
/// Annotation used for making text bold, commonly used in diagnostic messages.
225+
static var diagnosticText: Self {
226+
Self(color: .default, trait: .bold)
227+
}
228+
229+
/// Annotation used for error text.
230+
static var errorText: Self {
231+
Self(color: .red, trait: .bold)
232+
}
233+
234+
/// Annotation used for warning text.
235+
static var warningText: Self {
236+
Self(color: .yellow, trait: .bold)
237+
}
238+
239+
/// Annotation used for note text.
240+
static var noteText: Self {
241+
Self(color: .default, trait: .bold)
242+
}
243+
244+
/// Annotation used for remarks or less critical text.
245+
static var remarkText: Self {
246+
Self(color: .blue, trait: .bold)
247+
}
248+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Protocol that defines a standard interface for decorating diagnostic output in source code.
14+
/// This protocol is intended to be used by entities such as ``DiagnosticsFormatter`` and ``GroupedDiagnostics``
15+
/// to apply custom decorations to diagnostic messages, buffer outlines, and code highlights.
16+
///
17+
/// ## Conforming to `DiagnosticDecorator`:
18+
///
19+
/// To conform to the `DiagnosticDecorator` protocol, you must implement three required methods:
20+
///
21+
/// 1. `decorateMessage(_:basedOnSeverity:)`: For decorating diagnostic messages.
22+
/// 2. `decorateBufferOutline(_:)`: For decorating the outlines of source code buffers.
23+
/// 3. `decorateHighlight(_:)`: For decorating individual highlights within a source code snippet.
24+
///
25+
/// ## Customization:
26+
///
27+
/// The protocol is designed to be easily customizable. Developers can create their own entities that conform
28+
/// to `DiagnosticDecorator` to implement custom decorating logic. This allows for different visual representations,
29+
/// such as using ANSI colors, underscores, emoji-based or other markers, for diagnostics in source code.
30+
public protocol DiagnosticDecorator {
31+
/// Decorates a diagnostic message based on its severity level.
32+
///
33+
/// - Parameters:
34+
/// - message: The diagnostic message that needs to be decorated.
35+
/// - severity: The severity level associated with the diagnostic message.
36+
///
37+
/// - Returns: A decorated version of the diagnostic message, enhanced by visual cues like color, text styles, or other markers
38+
/// based on its severity level.
39+
func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String
40+
41+
/// Decorates the outline of a source code buffer to visually enhance its structure.
42+
///
43+
/// - Parameter bufferOutline: The string representation of the source code buffer outline.
44+
///
45+
/// - Returns: A decorated version of the buffer outline, improved with visual cues like color, text styles, or other markers.
46+
func decorateBufferOutline(_ bufferOutline: String) -> String
47+
48+
/// Decorates a highlight within a source code snippet to emphasize it.
49+
///
50+
/// - Parameter highlight: The text segment within the source code snippet that should be emphasized.
51+
///
52+
/// - Returns: A tuple containing:
53+
/// - `highlightedSourceCode`: A string that represents the decorated version of the original source code snippet.
54+
/// - `additionalHighlightedLine`: An optional string containing additional lines of highlighting, if applicable.
55+
///
56+
/// - Note: The method returns a tuple to offer more flexibility in decorating highlights.
57+
/// This allows for a variety of techniques to be used, such as ANSI codes for color
58+
/// and additional lines for contextual emphasis, which will be combined during the rendering process.
59+
func decorateHighlight(_ highlight: String) -> (highlightedSourceCode: String, additionalHighlightedLine: String?)
60+
}
61+
62+
extension DiagnosticDecorator {
63+
/// Decorates a ``DiagnosticMessage`` instance by delegating to the `decorateMessage(_:basedOnSeverity:)` method.
64+
///
65+
/// - Parameter diagnosticMessage: The ``DiagnosticMessage`` instance that encapsulates both the message and its severity level.
66+
///
67+
/// - Returns: A decorated version of the diagnostic message, determined by its severity level.
68+
public func decorateDiagnosticMessage(_ diagnosticMessage: DiagnosticMessage) -> String {
69+
decorateMessage(diagnosticMessage.message, basedOnSeverity: diagnosticMessage.severity)
70+
}
71+
}

0 commit comments

Comments
 (0)