Skip to content

Commit 14049c1

Browse files
Fix expansion state
1 parent 5b16bac commit 14049c1

File tree

5 files changed

+266
-60
lines changed

5 files changed

+266
-60
lines changed

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ struct IssueNavigatorOutlineView: NSViewControllerRepresentable {
3131

3232
func updateNSViewController(_ nsViewController: IssueNavigatorViewController, context: Context) {
3333
nsViewController.rowHeight = prefs.preferences.general.projectNavigatorSize.rowHeight
34+
35+
// Update the controller reference if needed
36+
if nsViewController.workspace !== workspace {
37+
nsViewController.workspace = workspace
38+
context.coordinator.workspace = workspace
39+
context.coordinator.setupObservers()
40+
}
3441
}
3542

3643
func makeCoordinator() -> Coordinator {
@@ -48,15 +55,48 @@ struct IssueNavigatorOutlineView: NSViewControllerRepresentable {
4855
}
4956

5057
func setupObservers() {
58+
// Cancel existing subscriptions
59+
cancellables.removeAll()
60+
5161
guard let viewModel = workspace?.issueNavigatorViewModel else { return }
5262

63+
// Listen for diagnostic changes
5364
viewModel.diagnosticsDidChangePublisher
65+
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
5466
.sink { [weak self] _ in
55-
DispatchQueue.main.async {
56-
self?.controller?.outlineView.reloadData()
67+
guard let controller = self?.controller else { return }
68+
69+
// Save current selection
70+
let selectedRows = controller.outlineView.selectedRowIndexes
71+
72+
// Reload data
73+
controller.outlineView.reloadData()
74+
75+
// Restore expansion state after reload
76+
controller.restoreExpandedState()
77+
78+
// Restore selection if possible
79+
if !selectedRows.isEmpty {
80+
controller.outlineView.selectRowIndexes(selectedRows, byExtendingSelection: false)
5781
}
5882
}
5983
.store(in: &cancellables)
84+
85+
// Listen for filter changes
86+
viewModel.$filterOptions
87+
.dropFirst() // Skip initial value
88+
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
89+
.sink { [weak self] _ in
90+
guard let controller = self?.controller else { return }
91+
92+
controller.outlineView.reloadData()
93+
controller.restoreExpandedState()
94+
}
95+
.store(in: &cancellables)
96+
}
97+
98+
deinit {
99+
cancellables.removeAll()
60100
}
61101
}
62102
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,54 @@ extension IssueNavigatorViewController: NSOutlineViewDelegate {
8989
outlineView.layoutSubtreeIfNeeded()
9090
}
9191

92+
func outlineViewItemDidExpand(_ notification: Notification) {
93+
if let node = notification.userInfo?["NSObject"] as? (any IssueNode) {
94+
if let fileNode = node as? FileIssueNode {
95+
fileNode.isExpanded = true
96+
expandedItems.insert(fileNode)
97+
workspace?.issueNavigatorViewModel?.setFileExpanded(fileNode.uri, isExpanded: true)
98+
} else if let projectNode = node as? ProjectIssueNode {
99+
projectNode.isExpanded = true
100+
}
101+
}
102+
}
103+
104+
func outlineViewItemDidCollapse(_ notification: Notification) {
105+
if let node = notification.userInfo?["NSObject"] as? (any IssueNode) {
106+
if let fileNode = node as? FileIssueNode {
107+
fileNode.isExpanded = false
108+
expandedItems.remove(fileNode)
109+
workspace?.issueNavigatorViewModel?.setFileExpanded(fileNode.uri, isExpanded: false)
110+
} else if let projectNode = node as? ProjectIssueNode {
111+
projectNode.isExpanded = false
112+
}
113+
}
114+
}
115+
116+
func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
117+
guard let uri = object as? String else { return nil }
118+
119+
if let fileNode = workspace?.issueNavigatorViewModel?.getFileNode(for: uri) {
120+
return fileNode
121+
}
122+
123+
if let rootNode = workspace?.issueNavigatorViewModel?.filteredRootNode,
124+
rootNode.id.uuidString == uri {
125+
return rootNode
126+
}
127+
128+
return nil
129+
}
130+
131+
func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
132+
if let fileNode = item as? FileIssueNode {
133+
return fileNode.uri
134+
} else if let projectNode = item as? ProjectIssueNode {
135+
return projectNode.id.uuidString
136+
}
137+
return nil
138+
}
139+
92140
/// Adds a tooltip to the issue row.
93141
func outlineView( // swiftlint:disable:this function_parameter_count
94142
_ outlineView: NSOutlineView,

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

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ final class IssueNavigatorViewController: NSViewController {
4141
/// to open the file a second time.
4242
var shouldSendSelectionUpdate: Bool = true
4343

44+
/// Key for storing expansion state in UserDefaults
45+
private var expansionStateKey: String {
46+
guard let workspaceURL = workspace?.workspaceFileManager?.folderUrl else {
47+
return "IssueNavigatorExpansionState"
48+
}
49+
return "IssueNavigatorExpansionState_\(workspaceURL.path.hashValue)"
50+
}
51+
4452
/// Setup the ``scrollView`` and ``outlineView``
4553
override func loadView() {
4654
self.scrollView = NSScrollView()
@@ -50,7 +58,8 @@ final class IssueNavigatorViewController: NSViewController {
5058
self.outlineView = NSOutlineView()
5159
self.outlineView.dataSource = self
5260
self.outlineView.delegate = self
53-
self.outlineView.autosaveExpandedItems = false
61+
self.outlineView.autosaveExpandedItems = true
62+
self.outlineView.autosaveName = workspace?.workspaceFileManager?.folderUrl.path ?? ""
5463
self.outlineView.headerView = nil
5564
self.outlineView.menu = IssueNavigatorMenu(self)
5665
self.outlineView.menu?.delegate = self
@@ -72,15 +81,14 @@ final class IssueNavigatorViewController: NSViewController {
7281
scrollView.hasHorizontalScroller = false
7382
scrollView.autohidesScrollers = true
7483

75-
outlineView.expandItem(outlineView.item(atRow: 0))
84+
loadExpansionState()
7685

77-
/// Get autosave expanded items.
78-
for row in 0..<outlineView.numberOfRows {
79-
if let item = outlineView.item(atRow: row) as? FileIssueNode {
80-
if outlineView.isItemExpanded(item) {
81-
expandedItems.insert(item)
82-
}
86+
// Expand the project node by default
87+
DispatchQueue.main.async { [weak self] in
88+
if let rootItem = self?.outlineView.item(atRow: 0) {
89+
self?.outlineView.expandItem(rootItem)
8390
}
91+
self?.restoreExpandedState()
8492
}
8593
}
8694

@@ -89,6 +97,7 @@ final class IssueNavigatorViewController: NSViewController {
8997
}
9098

9199
deinit {
100+
saveExpansionState()
92101
outlineView?.removeFromSuperview()
93102
scrollView?.removeFromSuperview()
94103
}
@@ -97,6 +106,45 @@ final class IssueNavigatorViewController: NSViewController {
97106
fatalError()
98107
}
99108

109+
/// Saves the current expansion state to UserDefaults
110+
private func saveExpansionState() {
111+
guard let viewModel = workspace?.issueNavigatorViewModel else { return }
112+
113+
let expandedUris = viewModel.getExpandedFileUris()
114+
let urisArray = Array(expandedUris)
115+
116+
UserDefaults.standard.set(urisArray, forKey: expansionStateKey)
117+
}
118+
119+
/// Loads the expansion state from UserDefaults
120+
private func loadExpansionState() {
121+
guard let viewModel = workspace?.issueNavigatorViewModel else { return }
122+
123+
if let urisArray = UserDefaults.standard.stringArray(forKey: expansionStateKey) {
124+
let expandedUris = Set(urisArray)
125+
viewModel.restoreExpandedFileUris(expandedUris)
126+
}
127+
}
128+
129+
/// Restores the expanded state of items based on their model state
130+
public func restoreExpandedState() {
131+
// Expand root if it should be expanded
132+
if let rootItem = outlineView.item(atRow: 0) as? ProjectIssueNode,
133+
rootItem.isExpanded {
134+
outlineView.expandItem(rootItem)
135+
}
136+
137+
// Expand file nodes based on their expansion state
138+
for row in 0..<outlineView.numberOfRows {
139+
if let fileItem = outlineView.item(atRow: row) as? FileIssueNode {
140+
if fileItem.isExpanded {
141+
outlineView.expandItem(fileItem)
142+
expandedItems.insert(fileItem)
143+
}
144+
}
145+
}
146+
}
147+
100148
/// Expand or collapse the folder on double click
101149
@objc
102150
private func onItemDoubleClicked() {
@@ -111,7 +159,9 @@ final class IssueNavigatorViewController: NSViewController {
111159
toggleExpansion(of: fileNode)
112160
openFileTab(fileUri: fileNode.uri)
113161
} else if let diagnosticNode = item as? DiagnosticIssueNode {
114-
openFileTab(fileUri: diagnosticNode.fileUri)
162+
openFileTab(fileUri: diagnosticNode.fileUri,
163+
line: diagnosticNode.diagnostic.range.start.line,
164+
column: diagnosticNode.diagnostic.range.start.character)
115165
}
116166
}
117167

@@ -125,13 +175,20 @@ final class IssueNavigatorViewController: NSViewController {
125175
}
126176
}
127177

128-
/// Opens a file as a permanent tab
178+
/// Opens a file as a permanent tab, optionally at a specific line and column
129179
@inline(__always)
130-
private func openFileTab(fileUri: String) {
180+
private func openFileTab(fileUri: String, line: Int? = nil, column: Int? = nil) {
131181
guard let fileURL = URL(string: fileUri),
132182
let file = workspace?.workspaceFileManager?.getFile(fileURL.path) else {
133183
return
134184
}
185+
135186
workspace?.editorManager?.activeEditor.openTab(file: file, asTemporary: false)
136187
}
188+
189+
/// Called when view will disappear - save state
190+
override func viewWillDisappear() {
191+
super.viewWillDisappear()
192+
saveExpansionState()
193+
}
137194
}

CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ final class ProjectNavigatorViewController: NSViewController {
101101

102102
outlineView.expandItem(outlineView.item(atRow: 0))
103103

104-
/// Get autosave expanded items.
104+
// Get autosave expanded items.
105105
for row in 0..<outlineView.numberOfRows {
106106
if let item = outlineView.item(atRow: row) as? CEWorkspaceFile {
107107
if outlineView.isItemExpanded(item) {
@@ -110,7 +110,7 @@ final class ProjectNavigatorViewController: NSViewController {
110110
}
111111
}
112112

113-
/// "No Filter Results" label.
113+
// "No Filter Results" label.
114114
noResultsLabel = NSTextField(labelWithString: "No Filter Results")
115115
noResultsLabel.isHidden = true
116116
noResultsLabel.font = NSFont.systemFont(ofSize: 16)

0 commit comments

Comments
 (0)