Watch folders added during a session !59
2 files changed, +31 −1
Layout: unified · split
Sources/OrgApp/WorkspaceModel.swift +15 −1
| @@ -23,6 +23,8 @@ public final class WorkspaceModel { | |||
| 23 | /// Resolved root folders, keyed by their resolved path, which is how the index stores them. | 23 | /// Resolved root folders, keyed by their resolved path, which is how the index stores them. |
| 24 | private var resolvedRoots: [String: URL] = [:] | 24 | private var resolvedRoots: [String: URL] = [:] |
| 25 | @ObservationIgnored private var watcher: AnyObject? | 25 | @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 | ||
| 26 | private var changeVersion = 0 | 28 | private var changeVersion = 0 |
| 27 | @ObservationIgnored private var overlayCache: (text: String, record: FileRecord)? | 29 | @ObservationIgnored private var overlayCache: (text: String, record: FileRecord)? |
| 28 | 30 | ||
| @@ -60,6 +62,7 @@ public final class WorkspaceModel { | |||
| 60 | roots.append(root) | 62 | roots.append(root) |
| 61 | try saveRoots() | 63 | try saveRoots() |
| 62 | resolveRoots() | 64 | resolveRoots() |
| 65 | rootsChanged() | ||
| 63 | await refresh() | 66 | await refresh() |
| 64 | } | 67 | } |
| 65 | 68 | ||
| @@ -69,12 +72,19 @@ public final class WorkspaceModel { | |||
| 69 | roots.removeAll { bookmarkPaths.contains($0.path) } | 72 | roots.removeAll { bookmarkPaths.contains($0.path) } |
| 70 | try saveRoots() | 73 | try saveRoots() |
| 71 | resolveRoots() | 74 | resolveRoots() |
| 75 | rootsChanged() | ||
| 72 | var change = IndexChange() | 76 | var change = IndexChange() |
| 73 | change.removals = files.filter { $0.root == path }.map(\.path) | 77 | change.removals = files.filter { $0.root == path }.map(\.path) |
| 74 | try index.apply(change) | 78 | try index.apply(change) |
| 75 | try reloadFiles() | 79 | try reloadFiles() |
| 76 | } | 80 | } |
| 77 | 81 | ||
| 82 | private func rootsChanged() { | ||
| 83 | #if os(macOS) | ||
| 84 | if watching { startWatching() } | ||
| 85 | #endif | ||
| 86 | } | ||
| 87 | |||
| 78 | private func saveRoots() throws { | 88 | private func saveRoots() throws { |
| 79 | try JSONEncoder().encode(roots).write(to: rootsFile, options: .atomic) | 89 | try JSONEncoder().encode(roots).write(to: rootsFile, options: .atomic) |
| 80 | } | 90 | } |
| @@ -154,8 +164,12 @@ public final class WorkspaceModel { | |||
| 154 | } | 164 | } |
| 155 | 165 | ||
| 156 | #if os(macOS) | 166 | #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`. | ||
| 158 | public func startWatching() { | 169 | public func startWatching() { |
| 170 | watching = true | ||
| 171 | (watcher as? FSEventsWatcher)?.stop() | ||
| 172 | watcher = nil | ||
| 159 | let roots = Array(resolvedRoots.values) | 173 | let roots = Array(resolvedRoots.values) |
| 160 | guard !roots.isEmpty else { return } | 174 | guard !roots.isEmpty else { return } |
| 161 | let fsWatcher = FSEventsWatcher(roots: roots.map { $0.resolvingSymlinksInPath() }) { [weak self] events in | 175 | let fsWatcher = FSEventsWatcher(roots: roots.map { $0.resolvingSymlinksInPath() }) { [weak self] events in |
Tests/OrgAppTests/AppTests.swift +16
| @@ -99,6 +99,22 @@ struct WorkspaceModelTests { | |||
| 99 | #expect(model.search("alpha").count == 1) | 99 | #expect(model.search("alpha").count == 1) |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 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 | |||
| 102 | @Test func changedFilesAreReindexed() async throws { | 118 | @Test func changedFilesAreReindexed() async throws { |
| 103 | let notes = try Folder() | 119 | let notes = try Folder() |
| 104 | let file = try notes.write("a.org", "* Old\n") | 120 | let file = try notes.write("a.org", "* Old\n") |