Skip to content

Commit 7dd8256

Browse files
First implementation of issue navigator
1 parent bee555c commit 7dd8256

12 files changed

+658
-21
lines changed

CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
3333
var editorManager: EditorManager? = EditorManager()
3434
var statusBarViewModel: StatusBarViewModel? = StatusBarViewModel()
3535
var utilityAreaModel: UtilityAreaViewModel? = UtilityAreaViewModel()
36+
// TODO: GRAB PROJECT NAME FROM ROOT FOLDER
37+
var issueNavigatorViewModel: IssueNavigatorViewModel? = IssueNavigatorViewModel(
38+
projectName: "Test"
39+
)
3640
var searchState: SearchState?
3741
var openQuicklyViewModel: OpenQuicklyViewModel?
3842
var commandsPaletteState: QuickActionsViewModel?

CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class LanguageServer {
2121
let binary: LanguageServerBinary
2222
/// A cache to hold responses from the server, to minimize duplicate server requests
2323
let lspCache = LSPCache()
24+
/// The workspace document that this server is associated with
25+
let workspace: WorkspaceDocument
2426

2527
/// Tracks documents and their associated objects.
2628
/// Use this property when adding new objects that need to track file data, or have a state associated with the
@@ -41,12 +43,14 @@ class LanguageServer {
4143
init(
4244
languageId: LanguageIdentifier,
4345
binary: LanguageServerBinary,
46+
workspace: WorkspaceDocument,
4447
lspInstance: InitializingServer,
4548
serverCapabilities: ServerCapabilities,
4649
rootPath: URL
4750
) {
4851
self.languageId = languageId
4952
self.binary = binary
53+
self.workspace = workspace
5054
self.lspInstance = lspInstance
5155
self.serverCapabilities = serverCapabilities
5256
self.rootPath = rootPath
@@ -71,6 +75,7 @@ class LanguageServer {
7175
static func createServer(
7276
for languageId: LanguageIdentifier,
7377
with binary: LanguageServerBinary,
78+
workspace: WorkspaceDocument,
7479
workspacePath: String
7580
) async throws -> LanguageServer {
7681
let executionParams = Process.ExecutionParameters(
@@ -87,6 +92,7 @@ class LanguageServer {
8792
return LanguageServer(
8893
languageId: languageId,
8994
binary: binary,
95+
workspace: workspace,
9096
lspInstance: server,
9197
serverCapabilities: capabilities,
9298
rootPath: URL(filePath: workspacePath)

CodeEdit/Features/LSP/Service/LSPService+Events.swift

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension LSPService {
1818
// Create a new Task to listen to the events
1919
let task = Task.detached { [weak self] in
2020
for await event in languageClient.lspInstance.eventSequence {
21-
await self?.handleEvent(event, for: key)
21+
await self?.handleEvent(event, for: languageClient)
2222
}
2323
}
2424
eventListeningTasks[key] = task
@@ -31,20 +31,28 @@ extension LSPService {
3131
}
3232
}
3333

34-
private func handleEvent(_ event: ServerEvent, for key: ClientKey) {
34+
private func handleEvent(
35+
_ event: ServerEvent,
36+
for languageClient: LanguageServer
37+
) {
3538
// TODO: Handle Events
36-
// switch event {
39+
switch event {
3740
// case let .request(id, request):
38-
// print("Request ID: \(id) for \(key.languageId.rawValue)")
39-
// handleRequest(request)
40-
// case let .notification(notification):
41-
// handleNotification(notification)
41+
// print("Request ID: \(id) for \(languageClient.languageId.rawValue)")
42+
// handleRequest(request, languageClient)
43+
case let .notification(notification):
44+
handleNotification(notification, languageClient)
4245
// case let .error(error):
43-
// print("Error from EventStream for \(key.languageId.rawValue): \(error)")
44-
// }
46+
// print("Error from EventStream for \(languageClient.languageId.rawValue): \(error)")
47+
default:
48+
return
49+
}
4550
}
4651

47-
private func handleRequest(_ request: ServerRequest) {
52+
private func handleRequest(
53+
_ request: ServerRequest,
54+
_ languageClient: LanguageServer
55+
) {
4856
// TODO: Handle Requests
4957
// switch request {
5058
// case let .workspaceConfiguration(params, _):
@@ -73,15 +81,20 @@ extension LSPService {
7381
// }
7482
}
7583

76-
private func handleNotification(_ notification: ServerNotification) {
84+
private func handleNotification(
85+
_ notification: ServerNotification,
86+
_ languageClient: LanguageServer
87+
) {
7788
// TODO: Handle Notifications
78-
// switch notification {
89+
switch notification {
7990
// case let .windowLogMessage(params):
8091
// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n")
8192
// case let .windowShowMessage(params):
8293
// print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n")
83-
// case let .textDocumentPublishDiagnostics(params):
84-
// print("textDocumentPublishDiagnostics: \(params)")
94+
case let .textDocumentPublishDiagnostics(params):
95+
print("textDocumentPublishDiagnostics: \(params.diagnostics)")
96+
languageClient.workspace.issueNavigatorViewModel?
97+
.updateDiagnostics(params: params)
8598
// case let .telemetryEvent(params):
8699
// print("telemetryEvent: \(params)")
87100
// case let .protocolCancelRequest(params):
@@ -90,6 +103,8 @@ extension LSPService {
90103
// print("protocolProgress: \(params)")
91104
// case let .protocolLogTrace(params):
92105
// print("protocolLogTrace: \(params)")
93-
// }
106+
default:
107+
return
108+
}
94109
}
95110
}

CodeEdit/Features/LSP/Service/LSPService.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ final class LSPService: ObservableObject {
173173
/// - Returns: The new language server.
174174
func startServer(
175175
for languageId: LanguageIdentifier,
176+
workspace: WorkspaceDocument,
176177
workspacePath: String
177178
) async throws -> LanguageServer {
178179
guard let serverBinary = languageConfigs[languageId] else {
@@ -184,6 +185,7 @@ final class LSPService: ObservableObject {
184185
let server = try await LanguageServer.createServer(
185186
for: languageId,
186187
with: serverBinary,
188+
workspace: workspace,
187189
workspacePath: workspacePath
188190
)
189191
languageClients[ClientKey(languageId, workspacePath)] = server
@@ -208,7 +210,11 @@ final class LSPService: ObservableObject {
208210
if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] {
209211
languageServer = server
210212
} else {
211-
languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath)
213+
languageServer = try await self.startServer(
214+
for: lspLanguage,
215+
workspace: workspace,
216+
workspacePath: workspacePath
217+
)
212218
}
213219
} catch {
214220
// swiftlint:disable:next line_length

CodeEdit/Features/NavigatorArea/IssueNavigator/IssueNavigatorView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
import SwiftUI
99

10+
/// # Issue Navigator - Sidebar
11+
///
12+
/// A list that functions as an issue navigator, showing collapsible issues
13+
/// within a project.
14+
///
1015
struct IssueNavigatorView: View {
1116
var body: some View {
12-
VStack {
13-
Spacer()
14-
}
17+
IssueNavigatorOutlineView()
1518
}
1619
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// IssueNavigatorOutlineView.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 3/16/25.
6+
//
7+
8+
import SwiftUI
9+
import Combine
10+
11+
/// Wraps an ``OutlineViewController`` inside a `NSViewControllerRepresentable`
12+
struct IssueNavigatorOutlineView: NSViewControllerRepresentable {
13+
14+
@EnvironmentObject var workspace: WorkspaceDocument
15+
@EnvironmentObject var editorManager: EditorManager
16+
17+
@StateObject var prefs: Settings = .shared
18+
19+
typealias NSViewControllerType = IssueNavigatorViewController
20+
21+
func makeNSViewController(context: Context) -> IssueNavigatorViewController {
22+
let controller = IssueNavigatorViewController()
23+
controller.workspace = workspace
24+
controller.editor = editorManager.activeEditor
25+
return controller
26+
}
27+
28+
func updateNSViewController(_ nsViewController: IssueNavigatorViewController, context: Context) {
29+
nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// IssueNavigatorViewController+NSOutlineViewDataSource.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 3/16/25.
6+
//
7+
8+
import AppKit
9+
10+
extension IssueNavigatorViewController: NSOutlineViewDataSource {
11+
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
12+
if item == nil {
13+
// Always show the project node
14+
return 1
15+
}
16+
if let node = item as? ProjectIssueNode {
17+
return node.files.count
18+
}
19+
if let node = item as? FileIssueNode {
20+
return node.diagnostics.count
21+
}
22+
return 0
23+
}
24+
25+
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
26+
if item == nil {
27+
return workspace?.issueNavigatorViewModel?.filteredRootNode as Any
28+
}
29+
if let node = item as? ProjectIssueNode {
30+
return node.files[index]
31+
}
32+
if let node = item as? FileIssueNode {
33+
return node.diagnostics[index]
34+
}
35+
36+
fatalError("Unexpected item type in IssueNavigator outlineView")
37+
}
38+
39+
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
40+
if let node = item as? any IssueNode {
41+
return node.isExpandable
42+
}
43+
return false
44+
}
45+
46+
func outlineView(
47+
_ outlineView: NSOutlineView,
48+
objectValueFor tableColumn: NSTableColumn?,
49+
byItem item: Any?
50+
) -> Any? {
51+
return item
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// IssueNavigatorViewController+NSOutlineViewDelegate.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 3/16/25.
6+
//
7+
8+
import AppKit
9+
10+
extension IssueNavigatorViewController: NSOutlineViewDelegate {
11+
func outlineView(
12+
_ outlineView: NSOutlineView,
13+
shouldShowCellExpansionFor tableColumn: NSTableColumn?,
14+
item: Any
15+
) -> Bool {
16+
true
17+
}
18+
19+
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool {
20+
true
21+
}
22+
23+
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
24+
guard let tableColumn else { return nil }
25+
26+
let frameRect = NSRect(x: 0, y: 0, width: tableColumn.width, height: rowHeight)
27+
let cell = StandardTableViewCell(frame: frameRect)
28+
if let node = item as? (any IssueNode) {
29+
cell.configLabel(
30+
label: NSTextField(string: node.name),
31+
isEditable: false
32+
)
33+
}
34+
return cell
35+
}
36+
37+
func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
38+
if item is DiagnosticIssueNode {
39+
let lines = Double(Settings.shared.preferences.general.issueNavigatorDetail.rawValue)
40+
return rowHeight * lines
41+
}
42+
return rowHeight // This can be changed to 20 to match Xcode's row height.
43+
}
44+
45+
/// Adds a tooltip to the issue row.
46+
func outlineView( // swiftlint:disable:this function_parameter_count
47+
_ outlineView: NSOutlineView,
48+
toolTipFor cell: NSCell,
49+
rect: NSRectPointer,
50+
tableColumn: NSTableColumn?,
51+
item: Any,
52+
mouseLocation: NSPoint
53+
) -> String {
54+
if let node = item as? (any IssueNode) {
55+
return node.name
56+
}
57+
return ""
58+
}
59+
}

0 commit comments

Comments
 (0)