Skip to content

Commit 6619d16

Browse files
Fix Autosave "changed by another application " Spam (#2072)
### Description Fixes an issue where a file save would not correctly update the `CodeFileDocument`'s metadata when saving. This caused scheduled autosave operations to fail with a false positive 'changed by another application' error. To fix, I'm allowing `NSDocument` to handle the actual file system operation, rather than writing the data like we were before. I've kept the extra logic in the overridden `save` method to fix broken directories. There's no good reason to move the file saving operation out of `NSDocument`, since that subclass likely handles it better than we do with an atomic data write. I've also moved autosave scheduling out of UI and into the document class. ### Related Issues * closes #2033 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots N/A
1 parent 8867f3f commit 6619d16

File tree

2 files changed

+37
-15
lines changed

2 files changed

+37
-15
lines changed

CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ final class CodeFileDocument: NSDocument, ObservableObject {
9494
isDocumentEditedSubject.eraseToAnyPublisher()
9595
}
9696

97+
/// A lock that ensures autosave scheduling happens correctly.
98+
private var autosaveTimerLock: NSLock = NSLock()
99+
/// Timer used to schedule autosave intervals.
100+
private var autosaveTimer: Timer?
101+
97102
// MARK: - NSDocument
98103

99104
override static var autosavesInPlace: Bool {
@@ -130,6 +135,8 @@ final class CodeFileDocument: NSDocument, ObservableObject {
130135
}
131136
}
132137

138+
// MARK: - Data
139+
133140
override func data(ofType _: String) throws -> Data {
134141
guard let sourceEncoding, let data = (content?.string as NSString?)?.data(using: sourceEncoding.nsValue) else {
135142
Self.logger.error("Failed to encode contents to \(self.sourceEncoding.debugDescription)")
@@ -138,6 +145,8 @@ final class CodeFileDocument: NSDocument, ObservableObject {
138145
return data
139146
}
140147

148+
// MARK: - Read
149+
141150
/// This function is used for decoding files.
142151
/// It should not throw error as unsupported files can still be opened by QLPreviewView.
143152
override func read(from data: Data, ofType _: String) throws {
@@ -161,6 +170,8 @@ final class CodeFileDocument: NSDocument, ObservableObject {
161170
NotificationCenter.default.post(name: Self.didOpenNotification, object: self)
162171
}
163172

173+
// MARK: - Autosave
174+
164175
/// Triggered when change occurred
165176
override func updateChangeCount(_ change: NSDocument.ChangeType) {
166177
super.updateChangeCount(change)
@@ -183,6 +194,31 @@ final class CodeFileDocument: NSDocument, ObservableObject {
183194
self.isDocumentEditedSubject.send(self.isDocumentEdited)
184195
}
185196

197+
/// If ``hasUnautosavedChanges`` is `true` and an autosave has not already been scheduled, schedules a new autosave.
198+
/// If ``hasUnautosavedChanges`` is `false`, cancels any scheduled timers and returns.
199+
///
200+
/// All operations are done with the ``autosaveTimerLock`` acquired (including the scheduled autosave) to ensure
201+
/// correct timing when scheduling or cancelling timers.
202+
override func scheduleAutosaving() {
203+
autosaveTimerLock.withLock {
204+
if self.hasUnautosavedChanges {
205+
guard autosaveTimer == nil else { return }
206+
autosaveTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] timer in
207+
self?.autosaveTimerLock.withLock {
208+
guard timer.isValid else { return }
209+
self?.autosaveTimer = nil
210+
self?.autosave(withDelegate: nil, didAutosave: nil, contextInfo: nil)
211+
}
212+
}
213+
} else {
214+
autosaveTimer?.invalidate()
215+
autosaveTimer = nil
216+
}
217+
}
218+
}
219+
220+
// MARK: - Close
221+
186222
override func close() {
187223
super.close()
188224
NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL)
@@ -199,7 +235,7 @@ final class CodeFileDocument: NSDocument, ObservableObject {
199235
let directory = fileURL.deletingLastPathComponent()
200236
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
201237

202-
try data(ofType: fileType ?? "").write(to: fileURL, options: .atomic)
238+
super.save(sender)
203239
} catch {
204240
presentError(error)
205241
}

CodeEdit/Features/Editor/Views/CodeFileView.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,6 @@ struct CodeFileView: View {
9191
}
9292
.store(in: &cancellables)
9393

94-
codeFile
95-
.contentCoordinator
96-
.textUpdatePublisher
97-
.debounce(for: 1.0, scheduler: DispatchQueue.main)
98-
.sink { _ in
99-
// updateChangeCount is automatically managed by autosave(), so no manual call is necessary
100-
codeFile.autosave(withImplicitCancellability: false) { error in
101-
if let error {
102-
CodeFileDocument.logger.error("Failed to autosave document, error: \(error)")
103-
}
104-
}
105-
}
106-
.store(in: &cancellables)
107-
10894
codeFile.undoManager = self.undoManager.manager
10995
}
11096

0 commit comments

Comments
 (0)