diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift index 0906bbcbfb..df1750159a 100644 --- a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentInspectorView.swift @@ -11,6 +11,7 @@ struct InternalDevelopmentInspectorView: View { var body: some View { Form { InternalDevelopmentNotificationsView() + InternalDevelopmentOutputView() } } } diff --git a/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift new file mode 100644 index 0000000000..6f86baee25 --- /dev/null +++ b/CodeEdit/Features/InspectorArea/InternalDevelopmentInspector/InternalDevelopmentOutputView.swift @@ -0,0 +1,54 @@ +// +// InternalDevelopmentOutputView.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import SwiftUI + +struct InternalDevelopmentOutputView: View { + var body: some View { + Section("Output Utility") { + Button("Error Log") { + pushLog(.error) + } + Button("Warning Log") { + pushLog(.warning) + } + Button("Info Log") { + pushLog(.info) + } + Button("Debug Log") { + pushLog(.debug) + } + } + + } + + func pushLog(_ level: UtilityAreaLogLevel) { + InternalDevelopmentOutputSource.shared.pushLog( + .init( + message: randomString(), + subsystem: "internal.development", + category: "Logs", + level: level + ) + ) + } + + func randomString() -> String { + let strings = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce molestie, dui et consectetur" + + "porttitor, orci lectus fermentum augue, eu faucibus lectus nisl id velit. Suspendisse in mi nunc. Aliquam" + + "non dolor eu eros mollis euismod. Praesent mollis mauris at ex dapibus ornare. Ut imperdiet" + + "finibus lacus ut aliquam. Vivamus semper, mauris in condimentum volutpat, quam erat eleifend ligula," + + "nec tincidunt sem ante et ex. Sed dui magna, placerat quis orci at, bibendum molestie massa. Maecenas" + + "velit nunc, vehicula eu venenatis vel, tincidunt id purus. Morbi eu dignissim arcu, sed ornare odio." + + "Nam vestibulum tempus nibh id finibus.").split(separator: " ") + let count = Int.random(in: 0..<25) + return (0.. { /// The configuration options this server supports. var serverCapabilities: ServerCapabilities + var logContainer: LanguageServerLogContainer + /// An instance of a language server, that may or may not be initialized private(set) var lspInstance: InitializingServer /// The path to the root of the project @@ -58,6 +60,7 @@ class LanguageServer { self.serverCapabilities = serverCapabilities self.rootPath = rootPath self.openFiles = LanguageServerFileMap() + self.logContainer = LanguageServerLogContainer(language: languageId) self.logger = Logger( subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer.\(languageId.rawValue)" diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift index b4baa73bb9..41d61f72ea 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -5,6 +5,7 @@ // Created by Abe Malla on 6/1/24. // +import Foundation import LanguageClient import LanguageServerProtocol @@ -32,64 +33,67 @@ extension LSPService { } private func handleEvent(_ event: ServerEvent, for key: ClientKey) { - // TODO: Handle Events -// switch event { -// case let .request(id, request): -// print("Request ID: \(id) for \(key.languageId.rawValue)") -// handleRequest(request) -// case let .notification(notification): -// handleNotification(notification) -// case let .error(error): -// print("Error from EventStream for \(key.languageId.rawValue): \(error)") -// } + guard let client = languageClient(for: key.languageId, workspacePath: key.workspacePath) else { + return + } + + switch event { + case let .request(_, request): + handleRequest(request, client: client) + case let .notification(notification): + handleNotification(notification, client: client) + case let .error(error): + logger.warning("Error from server \(key.languageId.rawValue, privacy: .public): \(error)") + } } - private func handleRequest(_ request: ServerRequest) { + private func handleRequest(_ request: ServerRequest, client: LanguageServerType) { // TODO: Handle Requests -// switch request { -// case let .workspaceConfiguration(params, _): -// print("workspaceConfiguration: \(params)") -// case let .workspaceFolders(handler): -// print("workspaceFolders: \(String(describing: handler))") -// case let .workspaceApplyEdit(params, _): -// print("workspaceApplyEdit: \(params)") + switch request { + // case let .workspaceConfiguration(params, _): + // print("workspaceConfiguration: \(params)") + // case let .workspaceFolders(handler): + // print("workspaceFolders: \(String(describing: handler))") + // case let .workspaceApplyEdit(params, _): + // print("workspaceApplyEdit: \(params)") // case let .clientRegisterCapability(params, _): // print("clientRegisterCapability: \(params)") // case let .clientUnregisterCapability(params, _): // print("clientUnregisterCapability: \(params)") -// case let .workspaceCodeLensRefresh(handler): -// print("workspaceCodeLensRefresh: \(String(describing: handler))") + // case let .workspaceCodeLensRefresh(handler): + // print("workspaceCodeLensRefresh: \(String(describing: handler))") // case let .workspaceSemanticTokenRefresh(handler): -// print("workspaceSemanticTokenRefresh: \(String(describing: handler))") -// case let .windowShowMessageRequest(params, _): -// print("windowShowMessageRequest: \(params)") -// case let .windowShowDocument(params, _): -// print("windowShowDocument: \(params)") -// case let .windowWorkDoneProgressCreate(params, _): -// print("windowWorkDoneProgressCreate: \(params)") -// -// default: -// print() -// } +// print("Refresh semantic tokens!", handler) + // case let .windowShowMessageRequest(params, _): + // print("windowShowMessageRequest: \(params)") + // case let .windowShowDocument(params, _): + // print("windowShowDocument: \(params)") + // case let .windowWorkDoneProgressCreate(params, _): + // print("windowWorkDoneProgressCreate: \(params)") + default: + return + } } - private func handleNotification(_ notification: ServerNotification) { + private func handleNotification(_ notification: ServerNotification, client: LanguageServerType) { // TODO: Handle Notifications -// switch notification { -// case let .windowLogMessage(params): -// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") + switch notification { + case let .windowLogMessage(message): + client.logContainer.appendLog(message) // case let .windowShowMessage(params): // print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") -// case let .textDocumentPublishDiagnostics(params): -// print("textDocumentPublishDiagnostics: \(params)") + // case let .textDocumentPublishDiagnostics(params): + // print("textDocumentPublishDiagnostics: \(params)") // case let .telemetryEvent(params): // print("telemetryEvent: \(params)") -// case let .protocolCancelRequest(params): -// print("protocolCancelRequest: \(params)") + // case let .protocolCancelRequest(params): + // print("protocolCancelRequest: \(params)") // case let .protocolProgress(params): // print("protocolProgress: \(params)") // case let .protocolLogTrace(params): // print("protocolLogTrace: \(params)") -// } + default: + return + } } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index f927dd9441..559413ec29 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -115,7 +115,7 @@ final class LSPService: ObservableObject { } /// Holds the active language clients - var languageClients: [ClientKey: LanguageServerType] = [:] + @Published var languageClients: [ClientKey: LanguageServerType] = [:] /// Holds the language server configurations for all the installed language servers var languageConfigs: [LanguageIdentifier: LanguageServerBinary] = [:] /// Holds all the event listeners for each active language client @@ -212,7 +212,7 @@ final class LSPService: ObservableObject { /// - Parameter document: The code document that was opened. func openDocument(_ document: CodeFileDocument) { guard let workspace = document.findWorkspace(), - let workspacePath = workspace.fileURL?.absoluteURL.path(), + let workspacePath = workspace.fileURL?.absolutePath, let lspLanguage = document.getLanguage().lspLanguage else { return } diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/ExtensionUtilityAreaOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/ExtensionUtilityAreaOutputSource.swift new file mode 100644 index 0000000000..ebfe6457db --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/ExtensionUtilityAreaOutputSource.swift @@ -0,0 +1,44 @@ +// +// ExtensionUtilityAreaOutputSource.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import OSLog +import LogStream + +extension LogMessage: @retroactive Identifiable, UtilityAreaOutputMessage { + public var id: String { + "\(date.timeIntervalSince1970)" + process + (subsystem ?? "") + (category ?? "") + } + + var level: UtilityAreaLogLevel { + switch type { + case .fault, .error: + .error + case .info, .default: + .info + case .debug: + .debug + default: + .info + } + } +} + +struct ExtensionUtilityAreaOutputSource: UtilityAreaOutputSource { + var id: String { + "extension_output" + extensionInfo.id + } + + let extensionInfo: ExtensionInfo + + func cachedMessages() -> [LogMessage] { + [] + } + + func streamMessages() -> AsyncStream { + LogStream.logs(for: extensionInfo.pid, flags: [.info, .historical, .processOnly]) + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/InternalDevelopmentOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/InternalDevelopmentOutputSource.swift new file mode 100644 index 0000000000..0f489b8920 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/InternalDevelopmentOutputSource.swift @@ -0,0 +1,44 @@ +// +// InternalDevelopmentOutputSource.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import Foundation + +class InternalDevelopmentOutputSource: UtilityAreaOutputSource { + static let shared = InternalDevelopmentOutputSource() + + struct Message: UtilityAreaOutputMessage { + var id: UUID = UUID() + + var message: String + var date: Date = Date() + var subsystem: String? + var category: String? + var level: UtilityAreaLogLevel + } + + var id: UUID = UUID() + private var logs: [Message] = [] + private(set) var streamContinuation: AsyncStream.Continuation + private var stream: AsyncStream + + init() { + (stream, streamContinuation) = AsyncStream.makeStream() + } + + func pushLog(_ log: Message) { + logs.append(log) + streamContinuation.yield(log) + } + + func cachedMessages() -> [Message] { + logs + } + + func streamMessages() -> AsyncStream { + stream + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift new file mode 100644 index 0000000000..60e29c0d89 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/Sources/LanguageServerLogContainer.swift @@ -0,0 +1,64 @@ +// +// LanguageServerLogContainer.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import OSLog +import LanguageServerProtocol + +class LanguageServerLogContainer: UtilityAreaOutputSource { + struct LanguageServerMessage: UtilityAreaOutputMessage { + let log: LogMessageParams + var id: UUID = UUID() + + var message: String { + log.message + } + + var level: UtilityAreaLogLevel { + switch log.type { + case .error: + .error + case .warning: + .warning + case .info: + .info + case .log: + .debug + } + } + + var date: Date = Date() + var subsystem: String? + var category: String? + } + + let id: String + + private var streamContinuation: AsyncStream.Continuation + private var stream: AsyncStream + private(set) var logs: [LanguageServerMessage] = [] + + init(language: LanguageIdentifier) { + id = language.rawValue + (stream, streamContinuation) = AsyncStream.makeStream( + bufferingPolicy: .bufferingNewest(0) + ) + } + + func appendLog(_ log: LogMessageParams) { + let message = LanguageServerMessage(log: log) + logs.append(message) + streamContinuation.yield(message) + } + + func cachedMessages() -> [LanguageServerMessage] { + logs + } + + func streamMessages() -> AsyncStream { + stream + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaLogLevel.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaLogLevel.swift new file mode 100644 index 0000000000..9a04194384 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaLogLevel.swift @@ -0,0 +1,52 @@ +// +// UtilityAreaLogLevel.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import SwiftUI + +enum UtilityAreaLogLevel { + case error + case warning + case info + case debug + + var iconName: String { + switch self { + case .error: + "exclamationmark.3" + case .warning: + "exclamationmark.2" + case .info: + "info" + case .debug: + "stethoscope" + } + } + + var color: Color { + switch self { + case .error: + return Color(red: 202.0/255.0, green: 27.0/255.0, blue: 0) + case .warning: + return Color(red: 255.0/255.0, green: 186.0/255.0, blue: 0) + case .info: + return .cyan + case .debug: + return .coolGray + } + } + + var backgroundColor: Color { + switch self { + case .error: + color.opacity(0.1) + case .warning: + color.opacity(0.2) + case .info, .debug: + .clear + } + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaOutputSource.swift b/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaOutputSource.swift new file mode 100644 index 0000000000..2ef76aa8a9 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/Model/UtilityAreaOutputSource.swift @@ -0,0 +1,23 @@ +// +// UtilityAreaOutputSource.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import OSLog +import LogStream + +protocol UtilityAreaOutputMessage: Identifiable { + var message: String { get } + var date: Date { get } + var subsystem: String? { get } + var category: String? { get } + var level: UtilityAreaLogLevel { get } +} + +protocol UtilityAreaOutputSource: Identifiable { + associatedtype Message: UtilityAreaOutputMessage + func cachedMessages() -> [Message] + func streamMessages() -> AsyncStream +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift b/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift deleted file mode 100644 index 5c76724374..0000000000 --- a/CodeEdit/Features/UtilityArea/OutputUtility/UtilityAreaOutputView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// UtilityAreaOutputView.swift -// CodeEdit -// -// Created by Austin Condiff on 5/25/23. -// - -import SwiftUI -import LogStream - -struct UtilityAreaOutputView: View { - @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - - @ObservedObject var extensionManager = ExtensionManager.shared - - @State var output: [LogMessage] = [] - - @State private var filterText = "" - - @State var selectedOutputSource: ExtensionInfo? - - var filteredOutput: [LogMessage] { - output.filter { item in - return filterText == "" ? true : item.message.contains(filterText) - } - } - - var body: some View { - UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { _ in - Group { - if selectedOutputSource == nil { - Text("No output") - .font(.system(size: 16)) - .foregroundColor(.secondary) - .frame(maxHeight: .infinity) - } else { - if let ext = selectedOutputSource { - ScrollView { - VStack(alignment: .leading) { - ForEach(filteredOutput, id: \.self) { item in - HStack { - Text(item.message) - .fontWeight(.semibold) - .fontDesign(.monospaced) - .foregroundColor(item.type.color) - Spacer() - } - .padding(.leading, 2) - } - } - .padding(5) - .frame(maxWidth: .infinity) - .rotationEffect(.radians(.pi)) - .scaleEffect(x: -1, y: 1, anchor: .center) - } - .rotationEffect(.radians(.pi)) - .scaleEffect(x: -1, y: 1, anchor: .center) - .task(id: ext.pid) { - output = [] - for await item in LogStream.logs(for: ext.pid, flags: [.info, .historical, .processOnly]) { - output.append(item) - } - } - } - } - } - .paneToolbar { - Picker("Output Source", selection: $selectedOutputSource) { - Text("All Sources") - .tag(nil as ExtensionInfo?) - ForEach(extensionManager.extensions) { - Text($0.name) - .tag($0 as ExtensionInfo?) - } - } - .buttonStyle(.borderless) - .labelsHidden() - .controlSize(.small) - Spacer() - UtilityAreaFilterTextField(title: "Filter", text: $filterText) - .frame(maxWidth: 175) - Button { - output = [] - } label: { - Image(systemName: "trash") - } - } - } - } -} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputLogList.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputLogList.swift new file mode 100644 index 0000000000..ae606b3b11 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputLogList.swift @@ -0,0 +1,101 @@ +// +// UtilityAreaOutputLogList.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import SwiftUI + +struct UtilityAreaOutputLogList: View { + let source: Source + + @State var output: [Source.Message] = [] + @Binding var filterText: String + var toolbar: () -> Toolbar + + init(source: Source, filterText: Binding, @ViewBuilder toolbar: @escaping () -> Toolbar) { + self.source = source + self._filterText = filterText + self.toolbar = toolbar + } + + var filteredOutput: [Source.Message] { + if filterText.isEmpty { + return output + } + return output.filter { item in + return filterText == "" ? true : item.message.contains(filterText) + } + } + + var body: some View { + List(filteredOutput.reversed()) { item in + VStack(spacing: 2) { + HStack(spacing: 0) { + Text(item.message) + .fontDesign(.monospaced) + .font(.system(size: 12, weight: .regular).monospaced()) + Spacer(minLength: 0) + } + HStack(spacing: 6) { + HStack(spacing: 4) { + Image(systemName: item.level.iconName) + .foregroundColor(.white) + .font(.system(size: 7, weight: .semibold)) + .frame(width: 12, height: 12) + .background( + RoundedRectangle(cornerRadius: 2) + .fill(item.level.color) + .aspectRatio(1.0, contentMode: .fit) + ) + Text(item.date.logFormatted()) + .fontWeight(.medium) + } + if let subsystem = item.subsystem { + HStack(spacing: 2) { + Image(systemName: "gearshape.2") + .font(.system(size: 8, weight: .regular)) + Text(subsystem) + } + } + if let category = item.category { + HStack(spacing: 2) { + Image(systemName: "square.grid.3x3") + .font(.system(size: 8, weight: .regular)) + Text(category) + } + } + Spacer(minLength: 0) + } + .foregroundStyle(.secondary) + .font(.system(size: 9, weight: .semibold).monospaced()) + } + .rotationEffect(.radians(.pi)) + .scaleEffect(x: -1, y: 1, anchor: .center) + .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } + .listRowBackground(item.level.backgroundColor) + } + .listStyle(.plain) + .listRowInsets(EdgeInsets()) + .rotationEffect(.radians(.pi)) + .scaleEffect(x: -1, y: 1, anchor: .center) + .task(id: source.id) { + output = source.cachedMessages() + for await item in source.streamMessages() { + output.append(item) + } + } + .paneToolbar { + toolbar() + Spacer() + UtilityAreaFilterTextField(title: "Filter", text: $filterText) + .frame(maxWidth: 175) + Button { + output.removeAll(keepingCapacity: true) + } label: { + Image(systemName: "trash") + } + } + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift new file mode 100644 index 0000000000..4d65d39d80 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputSourcePicker.swift @@ -0,0 +1,90 @@ +// +// UtilityAreaOutputSourcePicker.swift +// CodeEdit +// +// Created by Khan Winter on 7/18/25. +// + +import SwiftUI + +struct UtilityAreaOutputSourcePicker: View { + typealias Sources = UtilityAreaOutputView.Sources + + @EnvironmentObject private var workspace: WorkspaceDocument + + @AppSettings(\.developerSettings.showInternalDevelopmentInspector) + var showInternalDevelopmentInspector + + @Binding var selectedSource: Sources? + + @ObservedObject var extensionManager = ExtensionManager.shared + + @Service var lspService: LSPService + @State private var updater: UUID = UUID() + @State private var languageServerClients: [LSPService.LanguageServerType] = [] + + var body: some View { + Picker("Output Source", selection: $selectedSource) { + if selectedSource == nil { + Text("No Selected Output Source") + .italic() + .tag(Sources?.none) + Divider() + } + + if languageServerClients.isEmpty { + Text("No Language Servers") + } else { + ForEach(languageServerClients, id: \.languageId) { server in + Text(Sources.languageServer(server.logContainer).title) + .tag(Sources.languageServer(server.logContainer)) + } + } + + Divider() + + if extensionManager.extensions.isEmpty { + Text("No Extensions") + } else { + ForEach(extensionManager.extensions) { extensionInfo in + Text(Sources.extensions(.init(extensionInfo: extensionInfo)).title) + .tag(Sources.extensions(.init(extensionInfo: extensionInfo))) + } + } + + if showInternalDevelopmentInspector { + Divider() + Text(Sources.devOutput.title) + .tag(Sources.devOutput) + } + } + .id(updater) + .buttonStyle(.borderless) + .labelsHidden() + .controlSize(.small) + .onAppear { + updateLanguageServers(lspService.languageClients) + } + .onReceive(lspService.$languageClients) { clients in + updateLanguageServers(clients) + } + .onReceive(extensionManager.$extensions) { _ in + updater = UUID() + } + } + + func updateLanguageServers(_ clients: [LSPService.ClientKey: LSPService.LanguageServerType]) { + languageServerClients = clients + .compactMap { (key, value) in + if key.workspacePath == workspace.fileURL?.absolutePath { + return value + } + return nil + } + .sorted(by: { $0.languageId.rawValue < $1.languageId.rawValue }) + if selectedSource == nil, let client = languageServerClients.first { + selectedSource = Sources.languageServer(client.logContainer) + } + updater = UUID() + } +} diff --git a/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift new file mode 100644 index 0000000000..94bd5a5a7c --- /dev/null +++ b/CodeEdit/Features/UtilityArea/OutputUtility/View/UtilityAreaOutputView.swift @@ -0,0 +1,100 @@ +// +// UtilityAreaOutputView.swift +// CodeEdit +// +// Created by Austin Condiff on 5/25/23. +// + +import SwiftUI +import LogStream + +struct UtilityAreaOutputView: View { + enum Sources: Hashable { + case extensions(ExtensionUtilityAreaOutputSource) + case languageServer(LanguageServerLogContainer) + case devOutput + + var title: String { + switch self { + case .extensions(let source): + "Extension - \(source.extensionInfo.name)" + case .languageServer(let source): + "Language Server - \(source.id)" + case .devOutput: + "Internal Development Output" + } + } + + public static func == (_ lhs: Sources, _ rhs: Sources) -> Bool { + switch (lhs, rhs) { + case let (.extensions(lhs), .extensions(rhs)): + return lhs.id == rhs.id + case let (.languageServer(lhs), .languageServer(rhs)): + return lhs.id == rhs.id + case (.devOutput, .devOutput): + return true + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .extensions(let source): + hasher.combine(0) + hasher.combine(source.id) + case .languageServer(let source): + hasher.combine(1) + hasher.combine(source.id) + case .devOutput: + hasher.combine(2) + } + } + } + + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + + @State private var filterText: String = "" + @State private var selectedSource: Sources? + + var body: some View { + UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { _ in + Group { + if let selectedSource { + switch selectedSource { + case .extensions(let source): + UtilityAreaOutputLogList(source: source, filterText: $filterText) { + UtilityAreaOutputSourcePicker(selectedSource: $selectedSource) + } + case .languageServer(let source): + UtilityAreaOutputLogList(source: source, filterText: $filterText) { + UtilityAreaOutputSourcePicker(selectedSource: $selectedSource) + } + case .devOutput: + UtilityAreaOutputLogList( + source: InternalDevelopmentOutputSource.shared, + filterText: $filterText + ) { + UtilityAreaOutputSourcePicker(selectedSource: $selectedSource) + } + } + } else { + Text("No output") + .font(.system(size: 16)) + .foregroundColor(.secondary) + .frame(maxHeight: .infinity) + .paneToolbar { + UtilityAreaOutputSourcePicker(selectedSource: $selectedSource) + Spacer() + UtilityAreaFilterTextField(title: "Filter", text: $filterText) + .frame(maxWidth: 175) + Button { } label: { + Image(systemName: "trash") + } + .disabled(true) + } + } + } + } + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/OSLogType+Color.swift b/CodeEdit/Features/UtilityArea/Views/OSLogType+Color.swift deleted file mode 100644 index 94535886df..0000000000 --- a/CodeEdit/Features/UtilityArea/Views/OSLogType+Color.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// OSLogType+Color.swift -// CodeEdit -// -// Created by Wouter Hennen on 22/05/2023. -// - -import OSLog -import SwiftUI - -extension OSLogType { - var color: Color { - switch self { - case .error: - return .orange - case .debug, .default: - return .primary - case .fault: - return .red - case .info: - return .cyan - default: - return .green - } - } -} diff --git a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift index e3e9d0925a..5e2d664134 100644 --- a/CodeEdit/Features/WindowCommands/CodeEditCommands.swift +++ b/CodeEdit/Features/WindowCommands/CodeEditCommands.swift @@ -12,16 +12,18 @@ struct CodeEditCommands: Commands { private var sourceControlIsEnabled var body: some Commands { - MainCommands() - FileCommands() - ViewCommands() - FindCommands() - NavigateCommands() - TasksCommands() - if sourceControlIsEnabled { SourceControlCommands() } - EditorCommands() - ExtensionCommands() - WindowCommands() + Group { // SwiftUI limits to 9 items in an initializer, so we have to group every 9 items. + MainCommands() + FileCommands() + ViewCommands() + FindCommands() + NavigateCommands() + TasksCommands() + if sourceControlIsEnabled { SourceControlCommands() } + EditorCommands() + ExtensionCommands() + WindowCommands() + } HelpCommands() } } diff --git a/CodeEdit/Utils/Extensions/Date/Date+Formatted.swift b/CodeEdit/Utils/Extensions/Date/Date+Formatted.swift index 0d1f2d1056..54ada62530 100644 --- a/CodeEdit/Utils/Extensions/Date/Date+Formatted.swift +++ b/CodeEdit/Utils/Extensions/Date/Date+Formatted.swift @@ -36,4 +36,14 @@ extension Date { return formatter.string(from: self) } + + static var logFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss.SSSS" + return formatter + }() + + func logFormatted() -> String { + Self.logFormatter.string(from: self) + } }