Skip to content

Issue Navigator #2006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
var editorManager: EditorManager? = EditorManager()
var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel()
var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel()
// TODO: GRAB PROJECT NAME FROM ROOT FOLDER
var issueNavigatorViewModel: IssueNavigatorViewModel? = IssueNavigatorViewModel(
projectName: "Test"
)
var searchState: SearchState?
var openQuicklyViewModel: OpenQuicklyViewModel?
var commandsPaletteState: QuickActionsViewModel?
Expand Down
6 changes: 6 additions & 0 deletions CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class LanguageServer {
let binary: LanguageServerBinary
/// A cache to hold responses from the server, to minimize duplicate server requests
let lspCache = LSPCache()
/// The workspace document that this server is associated with
let workspace: WorkspaceDocument

/// Tracks documents and their associated objects.
/// Use this property when adding new objects that need to track file data, or have a state associated with the
Expand All @@ -41,12 +43,14 @@ class LanguageServer {
init(
languageId: LanguageIdentifier,
binary: LanguageServerBinary,
workspace: WorkspaceDocument,
lspInstance: InitializingServer,
serverCapabilities: ServerCapabilities,
rootPath: URL
) {
self.languageId = languageId
self.binary = binary
self.workspace = workspace
self.lspInstance = lspInstance
self.serverCapabilities = serverCapabilities
self.rootPath = rootPath
Expand All @@ -71,6 +75,7 @@ class LanguageServer {
static func createServer(
for languageId: LanguageIdentifier,
with binary: LanguageServerBinary,
workspace: WorkspaceDocument,
workspacePath: String
) async throws -> LanguageServer {
let executionParams = Process.ExecutionParameters(
Expand All @@ -87,6 +92,7 @@ class LanguageServer {
return LanguageServer(
languageId: languageId,
binary: binary,
workspace: workspace,
lspInstance: server,
serverCapabilities: capabilities,
rootPath: URL(filePath: workspacePath)
Expand Down
45 changes: 30 additions & 15 deletions CodeEdit/Features/LSP/Service/LSPService+Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension LSPService {
// Create a new Task to listen to the events
let task = Task.detached { [weak self] in
for await event in languageClient.lspInstance.eventSequence {
await self?.handleEvent(event, for: key)
await self?.handleEvent(event, for: languageClient)
}
}
eventListeningTasks[key] = task
Expand All @@ -31,20 +31,28 @@ extension LSPService {
}
}

private func handleEvent(_ event: ServerEvent, for key: ClientKey) {
private func handleEvent(
_ event: ServerEvent,
for languageClient: LanguageServer
) {
// TODO: Handle Events
// switch event {
switch event {
// case let .request(id, request):
// print("Request ID: \(id) for \(key.languageId.rawValue)")
// handleRequest(request)
// case let .notification(notification):
// handleNotification(notification)
// print("Request ID: \(id) for \(languageClient.languageId.rawValue)")
// handleRequest(request, languageClient)
case let .notification(notification):
handleNotification(notification, languageClient)
// case let .error(error):
// print("Error from EventStream for \(key.languageId.rawValue): \(error)")
// }
// print("Error from EventStream for \(languageClient.languageId.rawValue): \(error)")
default:
return
}
}

private func handleRequest(_ request: ServerRequest) {
private func handleRequest(
_ request: ServerRequest,
_ languageClient: LanguageServer
) {
// TODO: Handle Requests
// switch request {
// case let .workspaceConfiguration(params, _):
Expand Down Expand Up @@ -73,15 +81,20 @@ extension LSPService {
// }
}

private func handleNotification(_ notification: ServerNotification) {
private func handleNotification(
_ notification: ServerNotification,
_ languageClient: LanguageServer
) {
// TODO: Handle Notifications
// switch notification {
switch notification {
// case let .windowLogMessage(params):
// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n")
// 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.diagnostics)")
languageClient.workspace.issueNavigatorViewModel?
.updateDiagnostics(params: params)
// case let .telemetryEvent(params):
// print("telemetryEvent: \(params)")
// case let .protocolCancelRequest(params):
Expand All @@ -90,6 +103,8 @@ extension LSPService {
// print("protocolProgress: \(params)")
// case let .protocolLogTrace(params):
// print("protocolLogTrace: \(params)")
// }
default:
return
}
}
}
8 changes: 7 additions & 1 deletion CodeEdit/Features/LSP/Service/LSPService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ final class LSPService: ObservableObject {
/// - Returns: The new language server.
func startServer(
for languageId: LanguageIdentifier,
workspace: WorkspaceDocument,
workspacePath: String
) async throws -> LanguageServer {
guard let serverBinary = languageConfigs[languageId] else {
Expand All @@ -184,6 +185,7 @@ final class LSPService: ObservableObject {
let server = try await LanguageServer.createServer(
for: languageId,
with: serverBinary,
workspace: workspace,
workspacePath: workspacePath
)
languageClients[ClientKey(languageId, workspacePath)] = server
Expand All @@ -208,7 +210,11 @@ final class LSPService: ObservableObject {
if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] {
languageServer = server
} else {
languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath)
languageServer = try await self.startServer(
for: lspLanguage,
workspace: workspace,
workspacePath: workspacePath
)
}
} catch {
// swiftlint:disable:next line_length
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// IssueNavigatorView.swift
// CodeEdit
//
// Created by Abe Malla on 3/14/25.
//

import SwiftUI

/// # Issue Navigator - Sidebar
///
/// A list that functions as an issue navigator, showing collapsible issues
/// within a project.
///
struct IssueNavigatorView: View {
var body: some View {
IssueNavigatorOutlineView()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// IssueNavigatorOutlineView.swift
// CodeEdit
//
// Created by Abe Malla on 3/16/25.
//

import SwiftUI
import Combine

/// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable`
struct IssueNavigatorOutlineView: NSViewControllerRepresentable {

@EnvironmentObject var workspace: WorkspaceDocument
@EnvironmentObject var editorManager: EditorManager

@StateObject var prefs: Settings = .shared

typealias NSViewControllerType = IssueNavigatorViewController

func makeNSViewController(context: Context) -> IssueNavigatorViewController {
let controller = IssueNavigatorViewController()
controller.workspace = workspace
controller.editor = editorManager.activeEditor
return controller
}

func updateNSViewController(_ nsViewController: IssueNavigatorViewController, context: Context) {
nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// IssueNavigatorViewController+NSOutlineViewDataSource.swift
// CodeEdit
//
// Created by Abe Malla on 3/16/25.
//

import AppKit

extension IssueNavigatorViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil {
// If there are no issues, don't show the project node
if let rootNode = workspace?.issueNavigatorViewModel?.filteredRootNode {
return rootNode.files.isEmpty ? 0 : 1
}
return 0
}
if let node = item as? ProjectIssueNode {
return node.files.count
}
if let node = item as? FileIssueNode {
return node.diagnostics.count
}
return 0
}

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if item == nil {
return workspace?.issueNavigatorViewModel?.filteredRootNode as Any
}
if let node = item as? ProjectIssueNode {
return node.files[index]
}
if let node = item as? FileIssueNode {
return node.diagnostics[index]
}

fatalError("Unexpected item type in IssueNavigator outlineView")
}

func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let node = item as? any IssueNode {
return node.isExpandable
}
return false
}

func outlineView(
_ outlineView: NSOutlineView,
objectValueFor tableColumn: NSTableColumn?,
byItem item: Any?
) -> Any? {
return item
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// IssueNavigatorViewController+NSOutlineViewDelegate.swift
// CodeEdit
//
// Created by Abe Malla on 3/16/25.
//

import AppKit

extension IssueNavigatorViewController: NSOutlineViewDelegate {
func outlineView(
_ outlineView: NSOutlineView,
shouldShowCellExpansionFor tableColumn: NSTableColumn?,
item: Any
) -> Bool {
true
}

func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool {
true
}

func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let tableColumn else { return nil }

let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight)

if let node = item as? (any IssueNode) {
return IssueTableViewCell(frame: frameRect, node: node)
}
return TextTableViewCell(frame: frameRect, startingText: "Unknown item")
}

func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
if let diagnosticNode = item as? DiagnosticIssueNode {
let columnWidth = outlineView.tableColumns.first?.width ?? outlineView.frame.width
let indentationLevel = outlineView.level(forItem: item)
let indentationSpace = CGFloat(indentationLevel) * outlineView.indentationPerLevel
let availableWidth = columnWidth - indentationSpace - 24

// Create a temporary text field for measurement
let tempView = NSTextField(wrappingLabelWithString: diagnosticNode.name)
tempView.allowsDefaultTighteningForTruncation = false
tempView.cell?.truncatesLastVisibleLine = true
tempView.cell?.wraps = true
tempView.maximumNumberOfLines = Settings.shared.preferences.general.issueNavigatorDetail.rawValue
tempView.preferredMaxLayoutWidth = availableWidth

let height = tempView.sizeThatFits(
NSSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)
).height
return max(height + 8, rowHeight)
}
return rowHeight
}

func outlineViewColumnDidResize(_ notification: Notification) {
// Disable animations temporarily
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = 0

let indexes = IndexSet(integersIn: 0..<outlineView.numberOfRows)
outlineView.noteHeightOfRows(withIndexesChanged: indexes)

NSAnimationContext.endGrouping()
outlineView.layoutSubtreeIfNeeded()
}

/// Adds a tooltip to the issue row.
func outlineView( // swiftlint:disable:this function_parameter_count
_ outlineView: NSOutlineView,
toolTipFor cell: NSCell,
rect: NSRectPointer,
tableColumn: NSTableColumn?,
item: Any,
mouseLocation: NSPoint
) -> String {
if let node = item as? (any IssueNode) {
return node.name
}
return ""
}
}
Loading
Loading