import Foundation import Parsing import AppKit import AXExtension public struct EditorInformation { public struct LineAnnotation: Equatable, Hashable { public var type: String public var line: Int // 1-Based public var message: String public var originalAnnotation: String public var rect: CGRect? = nil public var isError: Bool { type == "Error" } } public struct SourceEditorContent: Equatable { /// The content of the source editor. public var content: String /// The content of the source editor in lines. Every line should ends with `\n`. public var lines: [String] /// The selection ranges of the source editor. public var selections: [CursorRange] /// The cursor position of the source editor. public var cursorPosition: CursorPosition /// The cursor position as offset. public var cursorOffset: Int /// Line annotations of the source editor. public var lineAnnotations: [LineAnnotation] public var selectedContent: String { if let range = selections.first { let startIndex = min( max(0, range.start.line), lines.endIndex - 1 ) let endIndex = min( max(startIndex, range.end.line), lines.endIndex - 1 ) let selectedContent = lines[startIndex...endIndex] return selectedContent.joined() } return "" } public init( content: String, lines: [String], selections: [CursorRange], cursorPosition: CursorPosition, cursorOffset: Int, lineAnnotationElements: [AXUIElement] ) { self.content = content self.lines = lines self.selections = selections self.cursorPosition = cursorPosition self.cursorOffset = cursorOffset self.lineAnnotations = lineAnnotationElements.map { var parsedLineAnnotation = EditorInformation.parseLineAnnotation($0.description) parsedLineAnnotation.rect = $0.rect return parsedLineAnnotation } } } public let editorContent: SourceEditorContent? public let selectedContent: String public let selectedLines: [String] public let documentURL: URL public let workspaceURL: URL public let projectRootURL: URL public let relativePath: String public let language: CodeLanguage public init( editorContent: SourceEditorContent?, selectedContent: String, selectedLines: [String], documentURL: URL, workspaceURL: URL, projectRootURL: URL, relativePath: String, language: CodeLanguage ) { self.editorContent = editorContent self.selectedContent = selectedContent self.selectedLines = selectedLines self.documentURL = documentURL self.workspaceURL = workspaceURL self.projectRootURL = projectRootURL self.relativePath = relativePath self.language = language } public func code(in range: CursorRange) -> String { return EditorInformation.code(in: editorContent?.lines ?? [], inside: range).code } public static func lines(in code: [String], containing range: CursorRange) -> [String] { guard !code.isEmpty else { return [] } guard range.start.line <= range.end.line else { return [] } let startIndex = min(max(0, range.start.line), code.endIndex - 1) let endIndex = min(max(startIndex, range.end.line), code.endIndex - 1) guard startIndex <= endIndex else { return [] } let selectedLines = code[startIndex...endIndex] return Array(selectedLines) } public static func code( in code: [String], inside range: CursorRange, ignoreColumns: Bool = false ) -> (code: String, lines: [String]) { guard range.start <= range.end else { return ("", []) } let rangeLines = lines(in: code, containing: range) if ignoreColumns { return (rangeLines.joined(), rangeLines) } var content = rangeLines if !content.isEmpty { let lastLine = content[content.endIndex - 1] let droppedEndIndex = lastLine.utf16.index( lastLine.utf16.startIndex, offsetBy: range.end.character, limitedBy: lastLine.utf16.endIndex ) ?? lastLine.utf16.endIndex content[content.endIndex - 1] = if droppedEndIndex > lastLine.utf16.startIndex { String(lastLine[.. LineAnnotation { let lineAnnotationParser = Parse(input: Substring.self) { PrefixUpTo(":") ":" PrefixUpTo(":") ":" Int.parser() Prefix(while: { _ in true }) }.map { (prefix: Substring, _: Substring, line: Int, message: Substring) in let type = String(prefix.split(separator: " ").first ?? prefix) return LineAnnotation( type: type.trimmingCharacters(in: .whitespacesAndNewlines), line: line, message: message.trimmingCharacters(in: .whitespacesAndNewlines), originalAnnotation: annotation ) } do { return try lineAnnotationParser.parse(annotation[...]) } catch { return .init(type: "", line: 0, message: annotation, originalAnnotation: annotation) } } }