Skip to content

Commit fd46a24

Browse files
Issue navigator coordinator, open file on select, right click options, refactors
1 parent 9829fa6 commit fd46a24

15 files changed

+530
-194
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "display-p3",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0.098",
9+
"green" : "0.161",
10+
"red" : "0.725"
11+
}
12+
},
13+
"idiom" : "universal"
14+
},
15+
{
16+
"appearances" : [
17+
{
18+
"appearance" : "luminosity",
19+
"value" : "dark"
20+
}
21+
],
22+
"color" : {
23+
"color-space" : "display-p3",
24+
"components" : {
25+
"alpha" : "1.000",
26+
"blue" : "0.255",
27+
"green" : "0.231",
28+
"red" : "0.855"
29+
}
30+
},
31+
"idiom" : "universal"
32+
}
33+
],
34+
"info" : {
35+
"author" : "xcode",
36+
"version" : 1
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "display-p3",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0.114",
9+
"green" : "0.741",
10+
"red" : "0.965"
11+
}
12+
},
13+
"idiom" : "universal"
14+
},
15+
{
16+
"appearances" : [
17+
{
18+
"appearance" : "luminosity",
19+
"value" : "dark"
20+
}
21+
],
22+
"color" : {
23+
"color-space" : "display-p3",
24+
"components" : {
25+
"alpha" : "1.000",
26+
"blue" : "0.271",
27+
"green" : "0.784",
28+
"red" : "0.965"
29+
}
30+
},
31+
"idiom" : "universal"
32+
}
33+
],
34+
"info" : {
35+
"author" : "xcode",
36+
"version" : 1
37+
}
38+
}

CodeEdit/Features/Editor/Models/EditorManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class EditorManager: ObservableObject {
2929
/// History of last-used editors.
3030
var activeEditorHistory: Deque<() -> Editor?> = []
3131

32-
/// notify listeners whenever tab selection changes on the active editor.
32+
/// Notify listeners whenever tab selection changes on the active editor.
3333
var tabBarTabIdSubject = PassthroughSubject<String?, Never>()
3434
var cancellable: AnyCancellable?
3535

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// IssueNavigatorMenu.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 4/3/25.
6+
//
7+
8+
import SwiftUI
9+
10+
final class IssueNavigatorMenu: NSMenu {
11+
var item: (any IssueNode)?
12+
13+
/// The workspace, for opening the item
14+
var workspace: WorkspaceDocument?
15+
16+
/// The `IssueNavigatorViewController` is being called from.
17+
/// By sending it, we can access it's variables and functions.
18+
var sender: IssueNavigatorViewController
19+
20+
init(_ sender: IssueNavigatorViewController) {
21+
self.sender = sender
22+
super.init(title: "Options")
23+
}
24+
25+
@available(*, unavailable)
26+
required init(coder _: NSCoder) {
27+
fatalError("init(coder:) has not been implemented")
28+
}
29+
30+
/// Creates a `NSMenuItem` depending on the given arguments
31+
/// - Parameters:
32+
/// - title: The title of the menu item
33+
/// - action: A `Selector` or `nil` of the action to perform.
34+
/// - key: A `keyEquivalent` of the menu item. Defaults to an empty `String`
35+
/// - Returns: A `NSMenuItem` which has the target `self`
36+
private func menuItem(_ title: String, action: Selector?, key: String = "") -> NSMenuItem {
37+
let mItem = NSMenuItem(title: title, action: action, keyEquivalent: key)
38+
mItem.target = self
39+
return mItem
40+
}
41+
42+
/// Configures the menu based on the current selection in the outline view.
43+
/// - Menu items get added depending on the amount of selected items.
44+
private func setupMenu() {
45+
guard item != nil else { return }
46+
47+
let copy = menuItem("Copy", action: #selector(copyIssue))
48+
let showInFinder = menuItem("Show in Finder", action: #selector(showInFinder))
49+
let revealInProjectNavigator = menuItem(
50+
"Reveal in Project Navigator",
51+
action: #selector(revealInProjectNavigator)
52+
)
53+
let openInTab = menuItem("Open in Tab", action: #selector(openInTab))
54+
let openWithExternalEditor = menuItem("Open with External Editor", action: #selector(openWithExternalEditor))
55+
56+
items = [
57+
copy,
58+
.separator(),
59+
showInFinder,
60+
revealInProjectNavigator,
61+
.separator(),
62+
openInTab,
63+
openWithExternalEditor,
64+
]
65+
}
66+
67+
/// Updates the menu for the selected item and hides it if no item is provided.
68+
override func update() {
69+
removeAllItems()
70+
setupMenu()
71+
}
72+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// IssueNavigatorMenuActions.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 4/3/25.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
extension IssueNavigatorMenu {
12+
/// - Returns: the currently selected `IssueNode` items in the outline view.
13+
func selectedNodes() -> [any IssueNode] {
14+
let selectedItems = sender.outlineView.selectedRowIndexes.compactMap {
15+
sender.outlineView.item(atRow: $0) as? (any IssueNode)
16+
}
17+
18+
if let menuItem = sender.outlineView.item(atRow: sender.outlineView.clickedRow) as? (any IssueNode) {
19+
if !selectedItems.contains(where: { $0.id == menuItem.id }) {
20+
return [menuItem]
21+
}
22+
}
23+
24+
return selectedItems
25+
}
26+
27+
/// Finds the file node that contains a diagnostic node
28+
private func findFileNode(for diagnosticNode: DiagnosticIssueNode) -> FileIssueNode? {
29+
// First try to find it by checking parents in the outline view
30+
if let parent = sender.outlineView.parent(forItem: diagnosticNode) as? FileIssueNode {
31+
return parent
32+
}
33+
34+
// Fallback: Look for a file with matching URI
35+
for row in 0..<sender.outlineView.numberOfRows {
36+
if let fileNode = sender.outlineView.item(atRow: row) as? FileIssueNode {
37+
if fileNode.uri == diagnosticNode.fileUri {
38+
return fileNode
39+
}
40+
}
41+
}
42+
43+
return nil
44+
}
45+
46+
/// - Returns: the relevant file nodes, converting diagnostic nodes to their parent file nodes if needed
47+
private func selectedFileNodes() -> [FileIssueNode] {
48+
let nodes = selectedNodes()
49+
var fileNodes = [FileIssueNode]()
50+
51+
for node in nodes {
52+
if let fileNode = node as? FileIssueNode {
53+
if !fileNodes.contains(where: { $0.id == fileNode.id }) {
54+
fileNodes.append(fileNode)
55+
}
56+
} else if let diagnosticNode = node as? DiagnosticIssueNode {
57+
if let fileNode = findFileNode(for: diagnosticNode),
58+
!fileNodes.contains(where: { $0.id == fileNode.id }) {
59+
fileNodes.append(fileNode)
60+
}
61+
}
62+
}
63+
64+
return fileNodes
65+
}
66+
67+
/// Copies the details of the issue node that was selected
68+
@objc
69+
func copyIssue() {
70+
let textsToCopy = selectedNodes().compactMap { node -> String? in
71+
if let diagnosticNode = node as? DiagnosticIssueNode {
72+
return diagnosticNode.name
73+
} else if let fileNode = node as? FileIssueNode {
74+
return fileNode.name
75+
} else {
76+
return node.name
77+
}
78+
}
79+
80+
if !textsToCopy.isEmpty {
81+
let pasteboard = NSPasteboard.general
82+
pasteboard.clearContents()
83+
pasteboard.writeObjects([textsToCopy.joined(separator: "\n") as NSString])
84+
}
85+
}
86+
87+
/// Action that opens **Finder** at the items location.
88+
@objc
89+
func showInFinder() {
90+
let fileURLs = selectedFileNodes().compactMap { URL(string: $0.uri) }
91+
NSWorkspace.shared.activateFileViewerSelecting(fileURLs)
92+
}
93+
94+
@objc
95+
func revealInProjectNavigator() {
96+
guard let fileNode = selectedFileNodes().first,
97+
let fileURL = URL(string: fileNode.uri),
98+
let workspaceFileManager = workspace?.workspaceFileManager,
99+
let file = workspaceFileManager.getFile(fileURL.path) else {
100+
return
101+
}
102+
workspace?.listenerModel.highlightedFileItem = file
103+
}
104+
105+
/// Action that opens the item, identical to clicking it.
106+
@objc
107+
func openInTab() {
108+
for fileNode in selectedFileNodes() {
109+
if let fileURL = URL(string: fileNode.uri),
110+
let workspaceFileManager = workspace?.workspaceFileManager,
111+
let file = workspaceFileManager.getFile(fileURL.path) {
112+
workspace?.editorManager?.activeEditor.openTab(file: file)
113+
}
114+
}
115+
}
116+
117+
/// Action that opens in an external editor
118+
@objc
119+
func openWithExternalEditor() {
120+
let fileURLs = selectedFileNodes().compactMap { URL(string: $0.uri)?.path }
121+
122+
if !fileURLs.isEmpty {
123+
let process = Process()
124+
process.launchPath = "/usr/bin/open"
125+
process.arguments = fileURLs
126+
try? process.run()
127+
}
128+
}
129+
}

CodeEdit/Features/NavigatorArea/IssueNavigator/OutlineView/IssueNavigatorOutlineView.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,41 @@ struct IssueNavigatorOutlineView: NSViewControllerRepresentable {
2222
let controller = IssueNavigatorViewController()
2323
controller.workspace = workspace
2424
controller.editor = editorManager.activeEditor
25+
26+
context.coordinator.controller = controller
27+
context.coordinator.setupObservers()
28+
2529
return controller
2630
}
2731

2832
func updateNSViewController(_ nsViewController: IssueNavigatorViewController, context: Context) {
2933
nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight
3034
}
35+
36+
func makeCoordinator() -> Coordinator {
37+
Coordinator(workspace: workspace)
38+
}
39+
40+
class Coordinator: NSObject {
41+
var cancellables = Set<AnyCancellable>()
42+
weak var workspace: WorkspaceDocument?
43+
weak var controller: IssueNavigatorViewController?
44+
45+
init(workspace: WorkspaceDocument?) {
46+
self.workspace = workspace
47+
super.init()
48+
}
49+
50+
func setupObservers() {
51+
guard let viewModel = workspace?.issueNavigatorViewModel else { return }
52+
53+
viewModel.diagnosticsDidChangePublisher
54+
.sink { [weak self] _ in
55+
DispatchQueue.main.async {
56+
self?.controller?.outlineView.reloadData()
57+
}
58+
}
59+
.store(in: &cancellables)
60+
}
61+
}
3162
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// IssueNavigatorViewController+NSMenuDelegate.swift.swift
3+
// CodeEdit
4+
//
5+
// Created by Abe Malla on 4/3/25.
6+
//
7+
8+
import AppKit
9+
10+
extension IssueNavigatorViewController: NSMenuDelegate {
11+
/// Once a menu gets requested by a `right click` setup the menu
12+
///
13+
/// If the right click happened outside a row this will result in no menu being shown.
14+
/// - Parameter menu: The menu that got requested
15+
func menuNeedsUpdate(_ menu: NSMenu) {
16+
let row = outlineView.clickedRow
17+
guard let menu = menu as? IssueNavigatorMenu else { return }
18+
19+
if row == -1 {
20+
menu.item = nil
21+
} else {
22+
if let item = outlineView.item(atRow: row) as? (any IssueNode) {
23+
menu.item = item
24+
menu.workspace = workspace
25+
} else {
26+
menu.item = nil
27+
}
28+
}
29+
menu.update()
30+
}
31+
}

CodeEdit/Features/NavigatorArea/IssueNavigator/OutlineView/IssueNavigatorViewController+NSOutlineViewDelegate.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ extension IssueNavigatorViewController: NSOutlineViewDelegate {
3131
return TextTableViewCell(frame: frameRect, startingText: "Unknown item")
3232
}
3333

34+
func outlineViewSelectionDidChange(_ notification: Notification) {
35+
guard let outlineView = notification.object as? NSOutlineView else { return }
36+
37+
// If multiple rows are selected, do not open any file.
38+
guard outlineView.selectedRowIndexes.count == 1 else { return }
39+
guard shouldSendSelectionUpdate else { return }
40+
41+
let selectedItem = outlineView.item(atRow: outlineView.selectedRow)
42+
43+
// Get the file and open it if not already opened
44+
if let fileURL = URL(
45+
string: (selectedItem as? FileIssueNode)?.uri ??
46+
(selectedItem as? DiagnosticIssueNode)?.fileUri ?? ""
47+
), !fileURL.path.isEmpty {
48+
shouldSendSelectionUpdate = false
49+
if let file = workspace?.workspaceFileManager?.getFile(fileURL.path),
50+
workspace?.editorManager?.activeEditor.selectedTab?.file != file {
51+
workspace?.editorManager?.activeEditor.openTab(file: file, asTemporary: true)
52+
}
53+
shouldSendSelectionUpdate = true
54+
}
55+
}
56+
3457
func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
3558
if let diagnosticNode = item as? DiagnosticIssueNode {
3659
let columnWidth = outlineView.tableColumns.first?.width ?? outlineView.frame.width

0 commit comments

Comments
 (0)