Report each buffer's changes as its own !60
merged
merged by cmc on 2026-10-06 00:16 UTC
· krz/orgstar:fix-buffer-changes into main
3 files changed, +41 −1
Layout: unified · split
Sources/OrgApp/DocumentSession.swift
+9
| @@ -221,6 +221,13 @@ public final class DocumentSession { |
| 221 | refresh() |
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 | /// The buffer changed through typing. The outline follows once typing pauses. |
231 | /// The buffer changed through typing. The outline follows once typing pauses. |
| 225 | public func bufferChanged() { |
232 | public func bufferChanged() { |
| 226 | refresh(outline: false) |
233 | refresh(outline: false) |
| @@ -556,6 +563,8 @@ public final class DocumentSession { |
| 556 | entry.conflict = "\(name) changed on disk, and the changes conflict with yours." |
563 | entry.conflict = "\(name) changed on disk, and the changes conflict with yours." |
| 557 | case .merged, .reloaded: |
564 | case .merged, .reloaded: |
| 558 | entry.conflict = nil |
565 | entry.conflict = nil |
| |
566 | // Edits merged with the disk's still need saving. |
| |
567 | scheduleAutosave(entry) |
| 559 | case .unchanged: |
568 | case .unchanged: |
| 560 | break |
569 | break |
| 561 | } |
570 | } |
Sources/Orgstar/EditorView.swift
+2 −1
| @@ -83,7 +83,8 @@ struct EditorView: NSViewRepresentable { |
| 83 | guard let document = session.document else { return nil } |
83 | guard let document = session.document else { return nil } |
| 84 | let mode: OrgEditor.Mode = session.isOrg ? .org : .plain(language: session.path.flatMap(FileTypes.language)) |
84 | let mode: OrgEditor.Mode = session.isOrg ? .org : .plain(language: session.path.flatMap(FileTypes.language)) |
| 85 | let editor = OrgEditor(document: document, frame: container.bounds, editable: document.isEditable, highlighter: Self.highlighter, mode: mode) |
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 | editor.onMessage = { [weak commands] in commands?.show($0) } |
88 | editor.onMessage = { [weak commands] in commands?.show($0) } |
| 88 | editor.onKeysPending = { [weak commands] keys, next in |
89 | editor.onKeysPending = { [weak commands] keys, next in |
| 89 | commands?.pendingKeys = keys |
90 | commands?.pendingKeys = keys |
Tests/OrgEditorAppKitTests/EditorTests.swift
+30
| @@ -252,6 +252,36 @@ struct BufferTests { |
| 252 | h.checkInSync() |
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 | /// The wiring the app uses: the editor is the session's buffer and reports typing. |
285 | /// The wiring the app uses: the editor is the session's buffer and reports typing. |
| 256 | @Test func sessionSavesWhatIsTyped() async throws { |
286 | @Test func sessionSavesWhatIsTyped() async throws { |
| 257 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-\(UUID().uuidString)") |
287 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-\(UUID().uuidString)") |