Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Fixed

- Fix the Insert Table toolbar button doing nothing when the editor pane had focus, and corrupting the document when clicked repeatedly (a second table was inserted inside the first table's cell); every click now inserts a clean, well-separated table regardless of which pane has focus (#278) -- thanks @rcuisnier for the report!
- Fix auto-reload silently breaking after an external editor's atomic save by making the local-volume watcher check fall back to the parent directory when the file is transiently missing; also guard resource-file watchers against remote volumes and tear down any prior watcher before re-arming (#478)
- Fix the Preview pane still auto-scrolling toward the editor when typing after Sync Panes was turned off mid-session; toggling Sync Panes now takes effect immediately (settling the panes on disable, re-syncing on enable) without needing to reopen the document (#441) -- thanks @gregwillits!
- Fix blank preview when opening saved or externally-originated documents: the real document file is no longer used as the preview's base resource, which WebKit on macOS 26 can silently refuse to load (e.g. files with the execute bit set by sync clients like OneDrive, or stale TCC/provenance state) (#431, #405) -- thanks @maskedspitz, @songjianbupt, and @craigrodger for the diagnosis!
- Improve render-path responsiveness: single-pass word/character counting, cached body-extraction regex, and bounded renderer polling with cancellation (#388)
Expand Down
7 changes: 5 additions & 2 deletions MacDown/Code/Document/MPDocument.m
Original file line number Diff line number Diff line change
Expand Up @@ -3856,11 +3856,14 @@ + (NSString *)toggleCheckboxAtIndex:(NSUInteger)index inMarkdown:(NSString *)mar

- (void)startFileWatching
{
// Tear down any previously-armed watcher first, so an early return below
// (nil URL, or a path that cannot be watched) can never leak a stale
// watcher for an old session. Related to #478.
[self stopFileWatching];

if (!self.fileURL || !self.fileURL.isFileURL)
return;

[self stopFileWatching];

if (![MPFileWatcher canWatchPath:self.fileURL.path])
return;

Expand Down
6 changes: 5 additions & 1 deletion MacDown/Code/Utility/MPFileWatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
/// YES if currently watching.
@property (nonatomic, readonly, getter=isWatching) BOOL watching;

/// YES if the path is on a local volume that should receive vnode watchers.
/// YES if the path can receive vnode watchers: it is non-nil, non-empty, and
/// resides on a local volume. When the file itself does not exist (e.g. during
/// an external editor's atomic rename-replace save) the locality check falls
/// back to the parent directory. Returns NO for nil, empty, non-local, or
/// otherwise unresolvable paths.
+ (BOOL)canWatchPath:(NSString *)path;

/// Create a watcher for the given path. Calls handler on the main queue
Expand Down
19 changes: 17 additions & 2 deletions MacDown/Code/Utility/MPFileWatcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,26 @@ + (BOOL)canWatchPath:(NSString *)path
if (!path.length)
return NO;

NSURL *url = [NSURL fileURLWithPath:path];
// NSURLVolumeIsLocalKey can only be read from a URL that resolves to an
// existing item. During an external editor's atomic rename-replace save the
// file transiently disappears, so probing the file itself would wrongly
// report the path as non-local and disable auto-reload. Fall back to the
// parent directory's volume in that case. Related to #478.
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *probePath = path;
if (![fileManager fileExistsAtPath:probePath])
probePath = [path stringByDeletingLastPathComponent];
if (!probePath.length || ![fileManager fileExistsAtPath:probePath])
return NO;

NSURL *url = [NSURL fileURLWithPath:probePath];
NSNumber *isLocal = nil;
NSError *error = nil;
if (![url getResourceValue:&isLocal
forKey:NSURLVolumeIsLocalKey error:NULL])
forKey:NSURLVolumeIsLocalKey error:&error])
{
NSLog(@"MPFileWatcher: cannot determine volume locality for %@: %@",
probePath, error.localizedDescription);
return NO;
}

Expand Down
6 changes: 6 additions & 0 deletions MacDown/Code/Utility/MPResourceWatcherSet.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ - (void)updateWatchedPaths:(NSSet<NSString *> *)paths

- (void)addWatcherForPath:(NSString *)path
{
// Skip paths that cannot receive vnode watchers (nil/empty or on a remote
// volume) before constructing a watcher, mirroring the guard in
// -[MPDocument startFileWatching]. Related to #478.
if (![MPFileWatcher canWatchPath:path])
return;

__weak MPResourceWatcherSet *weakSelf = self;
NSString *watchedPath = [path copy];

Expand Down
26 changes: 26 additions & 0 deletions MacDownTests/MPFileWatcherTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ - (void)testCannotWatchNilPath
XCTAssertFalse([MPFileWatcher canWatchPath:nil]);
}

- (void)testCannotWatchEmptyPath
{
XCTAssertFalse([MPFileWatcher canWatchPath:@""]);
}

- (void)testCanWatchNonexistentLocalPath
{
// A file that does not yet exist (e.g. mid atomic rename-replace save) on a
// local volume must still be considered watchable: the locality check falls
// back to the parent directory. Otherwise auto-reload silently dies during
// an external editor's save. Related to #478.
NSString *path = [self.testDirectory stringByAppendingPathComponent:@"gone.txt"];
XCTAssertFalse([self.fileManager fileExistsAtPath:path]);
XCTAssertTrue([MPFileWatcher canWatchPath:path]);
}

- (void)testCannotWatchPathWithNonexistentParent
{
// Neither the file nor its parent directory exists -> nothing to probe for
// volume locality, so watching is not possible.
NSString *path = [[self.testDirectory
stringByAppendingPathComponent:@"missing-dir"]
stringByAppendingPathComponent:@"file.txt"];
XCTAssertFalse([MPFileWatcher canWatchPath:path]);
}

#pragma mark - Event Handling

- (void)testHandlerCalledOnFileWrite
Expand Down
9 changes: 9 additions & 0 deletions MacDownTests/MPResourceWatcherSetTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ - (void)testSkipsNonexistentFiles
XCTAssertEqual(self.watcherSet.watchedPaths.count, 0u);
}

- (void)testSkipsUnwatchablePath
{
// An unwatchable path (here an empty string, but in practice a remote
// volume) must be rejected before a watcher is constructed and never
// stored. Related to #478.
[self.watcherSet updateWatchedPaths:[NSSet setWithObject:@""]];
XCTAssertEqual(self.watcherSet.watchedPaths.count, 0u);
}

- (void)testDelegateCalledOnChange
{
NSString *path = [self createTestFileWithName:@"watch.png"];
Expand Down
Loading