Reload the open document when its .comp package changes on disk - #116
Merged
robbietilton merged 1 commit intoSep 25, 2026
Merged
Conversation
When something else writes an open project's .comp package, the document follows it: same tab, same viewport, same selection where the layers still exist. Only a real change counts, unsaved work is never replaced without asking, and a package caught half written is left alone. - ProjectWatcher listens to the kernel's file system events for the package folder, its manifest and its images folder. No polling, and it sees writes from any process, coordinated or not, which NSFilePresenter would miss. Events are coalesced, and every event re-arms the watch by path because an atomic save swaps the package folder. - ProjectDigest hashes the manifest and every asset, so a package that was only touched (sync clients rewriting metadata, identical bytes saved again) is not a change. - ProjectController checks a change off the main thread, loads through the same ProjectStore path as an open, ignores its own saves and packages that fail to load, waits for an edit in progress to finish, and asks with a Revert / Keep Mine sheet when the document has unsaved changes; a hidden tab with unsaved changes is asked when it comes to the front. - EditorSession.reloadProject installs the snapshot while keeping the viewport, collapsed folders and selection. Undo history starts over, as after an open. No change to the .comp format. Closes robbietilton#103.
Owner
|
Merged, thanks — this is great, and it follows #103 exactly. One change on main: the fingerprint now reads the manifest plus each image's name and size instead of hashing every image's bytes. Every save and open takes a fresh digest, and on a large project reading all the PNGs held each save noticeably. Anything editing a project rewrites the manifest, and a PNG with changed pixels all but always changes size, so it still catches real edits. All your ExternalChangeTests pass with it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #103. Follows the requirements you listed there, point by point.
Event-driven, no polling, reload off the main thread.
ProjectWatcherlistens to the kernel's file system events (DispatchSource.makeFileSystemObjectSource) for the package folder, itsmanifest.jsonand itsimagesfolder. I went with vnode events instead ofNSFilePresenterafter checking how writers behave: a presenter is only told about writes that go throughNSFileCoordinator, and agents, scripts,git checkoutand a fair share of sync clients write plainly, so a presenter never fires for exactly the cases the issue is about. Events are coalesced (300 ms) and the reload goes throughProjectStore.shared.load, the same actor path as an open, so nothing blocks the main thread on big projects.Only reload on a real change.
ProjectDigesthashes the manifest and every asset's bytes. A package that was only touched (metadata rewritten by a sync client, identical bytes saved again) has the same digest and is ignored; there is no reload and no flicker. The digest is remembered after every open, save and reload.Half-written packages are ignored. If the load fails, the open document is left alone with no message; the next change on disk is checked afresh. A package caught mid-write also fails the digest match against nothing, so it waits too.
Never react to our own saves. The save path sets a flag for its duration and then records the digest of what it wrote, so the watch sees our own package as already known.
Unsaved work is never replaced without asking. A document with unsaved changes gets a sheet, Revert / Keep Mine. Keep Mine remembers the version on disk so it is not asked again until the package changes once more. A hidden tab with unsaved changes is asked when it comes to the front, not while it is out of sight. Edits in progress (brush, transform, sheets) defer the check with a backing-off retry rather than pulling the document out from under them.
No change to the .comp format. The reload installs the snapshot while keeping the viewport, collapsed folders and selection where the layers still exist; undo history starts over, as after an open (it is session-only, per
docs/project-format.md).Tests:
ExternalChangeTestscovers the digest (touch vs. content), the in-place reload keeping viewport and selection, an external save and an in-place manifest rewrite both reloading, our own save and metadata touches not reloading, a half-written manifest being ignored until it loads, and unsaved work staying put. The fullCompositorTeststarget passes here (Xcode 26.6, macOS 26.5);TiledLayerTests/paintingAtTheLayersEdgeDoesNotChangeItfailed once under the full parallel run and passed in isolation, unrelated to this change.I have not been able to test against Dropbox from here; happy to adjust anything that shows up in your run.
🤖 Generated with Claude Code