From bfedd111865722273956b3087201de8327af71d5 Mon Sep 17 00:00:00 2001 From: Feror Date: Tue, 2 Jun 2026 23:35:50 +0200 Subject: [PATCH] fix(workspace): refresh expanded folder contents --- Sources/SaplingApp/SaplingApp.swift | 10 ++++++ Sources/SaplingUI/WorkspaceTreeView.swift | 42 +++++++++++++++-------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/Sources/SaplingApp/SaplingApp.swift b/Sources/SaplingApp/SaplingApp.swift index daa625d..8f20552 100644 --- a/Sources/SaplingApp/SaplingApp.swift +++ b/Sources/SaplingApp/SaplingApp.swift @@ -62,6 +62,7 @@ private final class SaplingAppModel: ObservableObject { @Published var workspace: Workspace? @Published var workspaceChildren: [URL: [WorkspaceItem]] = [:] @Published var loadingTreeItemURLs: Set = [] + @Published var workspaceTreeRevision = 0 @Published var workspaceSelection: WorkspaceTreeSelection? @Published var selectedProject: Project? @Published var selectedDocument: MarkdownDocument? @@ -133,6 +134,7 @@ private final class SaplingAppModel: ObservableObject { self.workspace = workspace workspaceChildren = [:] loadingTreeItemURLs = [] + invalidateWorkspaceTree() workspaceSelection = nil selectedProject = nil selectedDocument = nil @@ -167,6 +169,7 @@ private final class SaplingAppModel: ObservableObject { var loadingURLs = loadingTreeItemURLs loadingURLs.insert(url) loadingTreeItemURLs = loadingURLs + invalidateWorkspaceTree() let workspaceManager = self.workspaceManager Task { do { @@ -176,6 +179,7 @@ private final class SaplingAppModel: ObservableObject { var loadedChildren = workspaceChildren loadedChildren[url] = children workspaceChildren = loadedChildren + invalidateWorkspaceTree() } catch { editorErrorMessage = "Unable to load \(item.displayName): \(error.localizedDescription)" logger.error("Failed to load workspace tree item: \(error)", category: .workspace) @@ -183,6 +187,7 @@ private final class SaplingAppModel: ObservableObject { var loadingURLs = loadingTreeItemURLs loadingURLs.remove(url) loadingTreeItemURLs = loadingURLs + invalidateWorkspaceTree() } } @@ -253,6 +258,10 @@ private final class SaplingAppModel: ObservableObject { configuration.recentWorkspaceURLs = Array(configuration.recentWorkspaceURLs.prefix(10)) save(configuration: configuration) } + + private func invalidateWorkspaceTree() { + workspaceTreeRevision &+= 1 + } } private struct MainWindow: View { @@ -265,6 +274,7 @@ private struct MainWindow: View { WorkspaceTreeView( workspace: model.workspace, isLoadingWorkspace: model.isLoadingWorkspace, + treeContentRevision: model.workspaceTreeRevision, selection: $model.workspaceSelection, childrenFor: model.children(for:), isLoadingChildren: model.isLoadingChildren(for:), diff --git a/Sources/SaplingUI/WorkspaceTreeView.swift b/Sources/SaplingUI/WorkspaceTreeView.swift index dbcca1a..1c746b1 100644 --- a/Sources/SaplingUI/WorkspaceTreeView.swift +++ b/Sources/SaplingUI/WorkspaceTreeView.swift @@ -11,6 +11,7 @@ public enum WorkspaceTreeSelection: Hashable, Sendable { public struct WorkspaceTreeView: View { private let workspace: Workspace? private let isLoadingWorkspace: Bool + private let treeContentRevision: Int @Binding private var selection: WorkspaceTreeSelection? private let childrenFor: (WorkspaceItem) -> [WorkspaceItem] private let isLoadingChildren: (WorkspaceItem) -> Bool @@ -22,6 +23,7 @@ public struct WorkspaceTreeView: View { public init( workspace: Workspace?, isLoadingWorkspace: Bool = false, + treeContentRevision: Int = 0, selection: Binding, childrenFor: @escaping (WorkspaceItem) -> [WorkspaceItem] = { $0.children ?? [] }, isLoadingChildren: @escaping (WorkspaceItem) -> Bool = { _ in false }, @@ -32,6 +34,7 @@ public struct WorkspaceTreeView: View { ) { self.workspace = workspace self.isLoadingWorkspace = isLoadingWorkspace + self.treeContentRevision = treeContentRevision self._selection = selection self.childrenFor = childrenFor self.isLoadingChildren = isLoadingChildren @@ -53,6 +56,7 @@ public struct WorkspaceTreeView: View { ForEach(workspace.items, id: \.stableTreeID) { item in WorkspaceItemRow( item: item, + treeContentRevision: treeContentRevision, selection: $selection, childrenFor: childrenFor, isLoadingChildren: isLoadingChildren, @@ -87,6 +91,7 @@ public struct WorkspaceTreeView: View { private struct WorkspaceItemRow: View { let item: WorkspaceItem + let treeContentRevision: Int @Binding var selection: WorkspaceTreeSelection? let childrenFor: (WorkspaceItem) -> [WorkspaceItem] let isLoadingChildren: (WorkspaceItem) -> Bool @@ -159,22 +164,26 @@ private struct WorkspaceItemRow: View { @ViewBuilder private var children: some View { - if isLoadingChildren(item) { - Label("Loading...", systemImage: "progress.indicator") - .foregroundStyle(.secondary) - } else { - ForEach(childrenFor(item), id: \.stableTreeID) { child in - WorkspaceItemRow( - item: child, - selection: $selection, - childrenFor: childrenFor, - isLoadingChildren: isLoadingChildren, - onExpandItem: onExpandItem, - onSelectFile: onSelectFile, - onSelectProject: onSelectProject - ) + Group { + if isLoadingChildren(item) { + Label("Loading...", systemImage: "progress.indicator") + .foregroundStyle(.secondary) + } else { + ForEach(childrenFor(item), id: \.stableTreeID) { child in + WorkspaceItemRow( + item: child, + treeContentRevision: treeContentRevision, + selection: $selection, + childrenFor: childrenFor, + isLoadingChildren: isLoadingChildren, + onExpandItem: onExpandItem, + onSelectFile: onSelectFile, + onSelectProject: onSelectProject + ) + } } } + .id(WorkspaceChildrenContentID(itemURL: item.stableTreeID, revision: treeContentRevision)) } private func iconName(for kind: WorkspaceFileKind) -> String { @@ -186,6 +195,11 @@ private struct WorkspaceItemRow: View { } } +private struct WorkspaceChildrenContentID: Hashable { + var itemURL: URL + var revision: Int +} + private extension WorkspaceItem { var stableTreeID: URL { switch self {