diff --git a/Sources/CartonHelpers/Parsers/DiagnosticsParser.swift b/Sources/CartonHelpers/Parsers/DiagnosticsParser.swift index 823aebb9..76a6cb06 100644 --- a/Sources/CartonHelpers/Parsers/DiagnosticsParser.swift +++ b/Sources/CartonHelpers/Parsers/DiagnosticsParser.swift @@ -76,7 +76,8 @@ public struct DiagnosticsParser: ProcessOutputParser { struct CustomDiagnostic { let kind: Kind let file: String - let line: String.SubSequence + /// The number of the row in the source file that the diagnosis is for. + let lineNumber: Int let char: String.SubSequence let code: String let message: String @@ -124,13 +125,11 @@ public struct DiagnosticsParser: ProcessOutputParser { .replacingOccurrences(of: ":", with: "") == String(currFile) else { continue } fileMessages.append( - .init( - kind: CustomDiagnostic - .Kind(rawValue: String(components[2] - .trimmingCharacters(in: .whitespaces))) ?? - .note, + CustomDiagnostic( + kind: CustomDiagnostic.Kind(rawValue: String(components[2].trimmingCharacters(in: .whitespaces))) ?? .note, file: file, - line: components[0], + // FIXME: We should handle this more gracefully than force-unwrapping it. + lineNumber: Int(components[0])!, char: components[1], code: String(lines[lineIdx]), message: components.dropFirst(3).joined(separator: ":") @@ -157,14 +156,13 @@ public struct DiagnosticsParser: ProcessOutputParser { for (file, messages) in diagnostics.sorted(by: { $0.key < $1.key }) { guard messages.count > 0 else { continue } terminal.write("\(" \(file) ", color: "[1m", "[7m")") // bold, reversed - terminal.write(" \(messages.first!.file)\(messages.first!.line)\n\n", inColor: .grey) - // Group messages that occur on sequential lines to provie a more readable output + terminal.write(" \(messages.first!.file)\(messages.first!.lineNumber)\n\n", inColor: .grey) + // Group messages that occur on sequential lines to provide a more readable output var groupedMessages = [[CustomDiagnostic]]() for message in messages { - if let lastLineStr = groupedMessages.last?.last?.line, - let lastLine = Int(lastLineStr), - let line = Int(message.line), - lastLine == line - 1 || lastLine == line + if let finalLineNumber = groupedMessages.last?.last?.lineNumber, + // `message.lineNumber` is the current line number. + finalLineNumber == message.lineNumber - 1 || finalLineNumber == message.lineNumber { groupedMessages[groupedMessages.count - 1].append(message) } else { @@ -180,15 +178,29 @@ public struct DiagnosticsParser: ProcessOutputParser { " \(" \(kind) ", color: message.kind.color, "[37;1m") \(message.message)\n" ) // 37;1: bright white } - let maxLine = messages.map(\.line.count).max() ?? 0 + let greatestLineNumber = messages.map(\.lineNumber).max() ?? 0 + let numberOfDigitsInGreatestLineNumber: Int = { + let (quotient, remainder) = greatestLineNumber.quotientAndRemainder(dividingBy: 10) + return quotient + (remainder == 0 ? 0 : 1) + }() for (offset, message) in messages.enumerated() { if offset > 0 { // Make sure we don't log the same line twice - if messages[offset - 1].line != message.line { - flush(messages: messages, message: message, maxLine: maxLine, terminal) + if messages[offset - 1].lineNumber != message.lineNumber { + flush( + messages: messages, + message: message, + minimumSizeForLineNumbering: numberOfDigitsInGreatestLineNumber, + terminal: terminal + ) } } else { - flush(messages: messages, message: message, maxLine: maxLine, terminal) + flush( + messages: messages, + message: message, + minimumSizeForLineNumbering: numberOfDigitsInGreatestLineNumber, + terminal: terminal + ) } } terminal.write("\n") @@ -196,35 +208,60 @@ public struct DiagnosticsParser: ProcessOutputParser { terminal.write("\n") } } - + + /// <#Description#> + /// - Parameters: + /// - messages: <#messages description#> + /// - message: <#message description#> + /// - minimumSizeForLineNumbering: The minimum space that must be reserved for line numbers, so that they are well-aligned in the output. + /// - terminal: <#terminal description#> func flush( messages: [CustomDiagnostic], message: CustomDiagnostic, - maxLine: Int, - _ terminal: InteractiveWriter + minimumSizeForLineNumbering: Int, + terminal: InteractiveWriter ) { // Get all diagnostics for a particular line. - let allChars = messages.filter { $0.line == message.line }.map(\.char) + let allChars = messages.filter { $0.lineNumber == message.lineNumber }.map(\.char) // Output the code for this line, syntax highlighted - let paddedLine = message.line.padding(toLength: maxLine, withPad: " ", startingAt: 0) - let highlightedCode = Self.highlighter.highlight(message.code) - terminal - .write( - " \("\(paddedLine) | ", color: "[36m")\(highlightedCode)\n" - ) // 36: cyan - terminal.write( - " " + "".padding(toLength: maxLine, withPad: " ", startingAt: 0) + " | ", - inColor: .cyan - ) + /// A base-10 representation of the number of the row that the diagnosis is for, aligned vertically with all other rows. + let verticallyAlignedLineNumber = String(message.lineNumber, radix: 10).padding(toLength: minimumSizeForLineNumbering, withPad: " ", startingAt: 0) + /// The line of code that the diagnostics message is for. + let sourceLine = message.code + // The following 2 assignments remove the leading whitespace from each line of code in the diagnostics. + // Although technically, we should remove only horizontal whitespace characters, but since there is no vertical whitespace in a continuous line, we can safely remove all whitespace characters. + /// The position of the first non-whitespace character in the line of code that the diagnostics message is for. + /// + /// If no such character exists, then the position is the same as the line's `endIndex`. + let firstIndexOfNonWhitespaceCharacterInSourceLine = sourceLine.firstIndex(where: { !$0.isWhitespace } ) ?? sourceLine.endIndex + /// The line of code that the diagnostics message is for, with leading whitespace removed. + let sourceLineSansWhitespace = sourceLine[firstIndexOfNonWhitespaceCharacterInSourceLine...] + let highlightedCode = Self.highlighter.highlight(String(sourceLineSansWhitespace)) + // Each line of diagnostics output is indented with 2 spaces. + terminal.write(" \(verticallyAlignedLineNumber) | ", inColor: .cyan) + terminal.write(" \(highlightedCode)\n") + terminal.write(" \(String(repeating: " ", count: minimumSizeForLineNumbering)) | ", inColor: .cyan) // Aggregate the indicators (^ point to the error) onto a single line - var charIndicators = String(repeating: " ", count: Int(message.char)!) + "^" + + // Remove leading whitespace. + var charIndicators = String(repeating: " ", count: Int(message.char)! - (sourceLine.count - sourceLineSansWhitespace.count)) + "^" if allChars.count > 0 { for char in allChars.dropFirst() { let idx = Int(char)! if idx >= charIndicators.count { - charIndicators - .append(String(repeating: " ", count: idx - charIndicators.count) + "^") + for index in charIndicators.count.. 0 - { + if problem.lineNumber > 0 { var fileContents: String? if let fileBuf = fileBufs.first(where: { $0.path == problem.file })?.contents { fileContents = fileBuf @@ -283,9 +282,9 @@ public struct TestsParser: ProcessOutputParser { } if let fileContents = fileContents { let fileLines = fileContents.components(separatedBy: .newlines) - guard fileLines.count >= lineNum else { break } - let highlightedCode = Self.highlighter.highlight(String(fileLines[lineNum - 1])) - terminal.write(" \("\(problem.line) | ", color: "[36m")\(highlightedCode)\n") + guard fileLines.count >= problem.lineNumber else { break } + let highlightedCode = Self.highlighter.highlight(String(fileLines[problem.lineNumber - 1])) + terminal.write(" \("\(problem.lineNumber) | ", color: "[36m")\(highlightedCode)\n") } } }