Commit b12f088c30
b12f088c30ff9d57250fd32dc3b2b24b255b3575
parent: 618ad70f1a
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-06 00:16 UTC
Report each buffer's changes as its own
Every editor's onChange updated the current buffer, so a change in a
background buffer's editor (an agenda edit, a merge from disk) never
scheduled that buffer's autosave. Editors report their own path, and a
merge from disk schedules the autosave its result needs.
Layout: unified · split
Sources/OrgApp/DocumentSession.swift
+9
| @@ -221,6 +221,13 @@ public final class DocumentSession { |
| 221 | 221 | refresh() |
| 222 | 222 | } |
| 223 | 223 | |
| 224 | /// The buffer of `path` changed in its editor: the current one updates its views, another |
| 225 | /// saves on its own. |
| 226 | public func bufferChanged(_ path: String) { |
| 227 | guard let entry = entry(path) else { return } |
| 228 | if entry === current { bufferChanged() } else { scheduleAutosave(entry); refreshLists() } |
| 229 | } |
| 230 | |
| 224 | 231 | /// The buffer changed through typing. The outline follows once typing pauses. |
| 225 | 232 | public func bufferChanged() { |
| 226 | 233 | refresh(outline: false) |
| @@ -556,6 +563,8 @@ public final class DocumentSession { |
| 556 | 563 | entry.conflict = "\(name) changed on disk, and the changes conflict with yours." |
| 557 | 564 | case .merged, .reloaded: |
| 558 | 565 | entry.conflict = nil |
| 566 | // Edits merged with the disk's still need saving. |
| 567 | scheduleAutosave(entry) |
| 559 | 568 | case .unchanged: |
| 560 | 569 | break |
| 561 | 570 | } |
Sources/Orgstar/EditorView.swift
+2 −1
| @@ -83,7 +83,8 @@ struct EditorView: NSViewRepresentable { |
| 83 | 83 | guard let document = session.document else { return nil } |
| 84 | 84 | let mode: OrgEditor.Mode = session.isOrg ? .org : .plain(language: session.path.flatMap(FileTypes.language)) |
| 85 | 85 | let editor = OrgEditor(document: document, frame: container.bounds, editable: document.isEditable, highlighter: Self.highlighter, mode: mode) |
| 86 | | editor.onChange = { [weak session] in session?.bufferChanged() } |
| 86 | let path = session.path ?? "" |
| 87 | editor.onChange = { [weak session] in session?.bufferChanged(path) } |
| 87 | 88 | editor.onMessage = { [weak commands] in commands?.show($0) } |
| 88 | 89 | editor.onKeysPending = { [weak commands] keys, next in |
| 89 | 90 | commands?.pendingKeys = keys |
Tests/OrgEditorAppKitTests/EditorTests.swift
+30
| @@ -252,6 +252,36 @@ struct BufferTests { |
| 252 | 252 | h.checkInSync() |
| 253 | 253 | } |
| 254 | 254 | |
| 255 | /// Each buffer's editor reports its own changes: a background buffer's edit saves its |
| 256 | /// file, not the shown one's. |
| 257 | @Test func backgroundEditorsSaveTheirOwnFiles() async throws { |
| 258 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-\(UUID().uuidString)") |
| 259 | try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) |
| 260 | defer { try? FileManager.default.removeItem(at: folder) } |
| 261 | let a = folder.appendingPathComponent("a.org"), b = folder.appendingPathComponent("b.org") |
| 262 | try Data("* A\n".utf8).write(to: a) |
| 263 | try Data("* B\n".utf8).write(to: b) |
| 264 | let session = DocumentSession(recovery: FileRecoveryStore(directory: folder.appendingPathComponent("r"))) |
| 265 | session.autosaveDelay = .milliseconds(10) |
| 266 | var harnesses: [String: Harness] = [:] |
| 267 | for file in [a, b] { |
| 268 | session.open(file.path) |
| 269 | let h = Harness(try #require(session.document)) |
| 270 | let path = file.path |
| 271 | h.editor.onChange = { [weak session] in session?.bufferChanged(path) } |
| 272 | session.attach(h.editor) |
| 273 | harnesses[path] = h |
| 274 | } |
| 275 | #expect(session.path == b.path) |
| 276 | harnesses[a.path]!.textView.insertText("x", replacementRange: NSRange(location: 0, length: 0)) |
| 277 | #expect(session.dirtyPaths == [a.path]) |
| 278 | #expect(!session.isDirty) |
| 279 | for _ in 0..<50 where (try? String(contentsOf: a, encoding: .utf8)) != "x* A\n" { |
| 280 | try await Task.sleep(for: .milliseconds(20)) |
| 281 | } |
| 282 | #expect(try String(contentsOf: a, encoding: .utf8) == "x* A\n") |
| 283 | } |
| 284 | |
| 255 | 285 | /// The wiring the app uses: the editor is the session's buffer and reports typing. |
| 256 | 286 | @Test func sessionSavesWhatIsTyped() async throws { |
| 257 | 287 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-\(UUID().uuidString)") |