Watch folders added during a session !59

merged merged by cmc on 2026-10-06 00:14 UTC · krz/orgstar:fix-watch-new-roots into main

2 files changed, +31 −1

Layout: unified · split

Sources/OrgApp/WorkspaceModel.swift +15 −1
@@ -23,6 +23,8 @@ public final class WorkspaceModel {
2323 /// Resolved root folders, keyed by their resolved path, which is how the index stores them.
2424 private var resolvedRoots: [String: URL] = [:]
2525 @ObservationIgnored private var watcher: AnyObject?
26 /// Set once watching starts, so adding or removing a root watches the new set.
27 @ObservationIgnored private var watching = false
2628 private var changeVersion = 0
2729 @ObservationIgnored private var overlayCache: (text: String, record: FileRecord)?
2830
@@ -60,6 +62,7 @@ public final class WorkspaceModel {
6062 roots.append(root)
6163 try saveRoots()
6264 resolveRoots()
65 rootsChanged()
6366 await refresh()
6467 }
6568
@@ -69,12 +72,19 @@ public final class WorkspaceModel {
6972 roots.removeAll { bookmarkPaths.contains($0.path) }
7073 try saveRoots()
7174 resolveRoots()
75 rootsChanged()
7276 var change = IndexChange()
7377 change.removals = files.filter { $0.root == path }.map(\.path)
7478 try index.apply(change)
7579 try reloadFiles()
7680 }
7781
82 private func rootsChanged() {
83 #if os(macOS)
84 if watching { startWatching() }
85 #endif
86 }
87
7888 private func saveRoots() throws {
7989 try JSONEncoder().encode(roots).write(to: rootsFile, options: .atomic)
8090 }
@@ -154,8 +164,12 @@ public final class WorkspaceModel {
154164 }
155165
156166 #if os(macOS)
157 /// Watches every root; changes reconcile the index and are reported through `changedFile`.
167 /// Watches every root, including roots added later; changes reconcile the index and are
168 /// reported through `changedFile`.
158169 public func startWatching() {
170 watching = true
171 (watcher as? FSEventsWatcher)?.stop()
172 watcher = nil
159173 let roots = Array(resolvedRoots.values)
160174 guard !roots.isEmpty else { return }
161175 let fsWatcher = FSEventsWatcher(roots: roots.map { $0.resolvingSymlinksInPath() }) { [weak self] events in
Tests/OrgAppTests/AppTests.swift +16
@@ -99,6 +99,22 @@ struct WorkspaceModelTests {
9999 #expect(model.search("alpha").count == 1)
100100 }
101101
102 /// Watching starts with no folders (a first launch); a folder added afterwards is
103 /// watched.
104 @Test func foldersAddedLaterAreWatched() async throws {
105 let notes = try Folder()
106 let model = try WorkspaceModel(directory: nil)
107 model.startWatching()
108 try await model.addRoot(notes.url)
109 let file = try notes.write("new.org", "* Fresh heading\n")
110 var found = false
111 for _ in 0..<40 where !found {
112 try await Task.sleep(for: .milliseconds(100))
113 found = model.files.contains { URL(fileURLWithPath: $0.path).resolvingSymlinksInPath() == file.resolvingSymlinksInPath() }
114 }
115 #expect(found)
116 }
117
102118 @Test func changedFilesAreReindexed() async throws {
103119 let notes = try Folder()
104120 let file = try notes.write("a.org", "* Old\n")