@@ -3,10 +3,10 @@ import CopilotModel
33import CopilotService
44import Environment
55import Foundation
6+ import Logger
67import Preferences
78import SuggestionInjector
89import XPCShared
9- import Logger
1010
1111@ServiceActor
1212final class Filespace {
@@ -40,8 +40,15 @@ final class Filespace {
4040 Environment . now ( ) . timeIntervalSince ( lastSuggestionUpdateTime) > 60 * 60 * 8
4141 }
4242
43- fileprivate init ( fileURL: URL ) {
43+ let fileSaveWatcher : FileSaveWatcher
44+
45+ fileprivate init ( fileURL: URL , onSave: @escaping ( Filespace ) -> Void ) {
4446 self . fileURL = fileURL
47+ fileSaveWatcher = . init( fileURL: fileURL)
48+ fileSaveWatcher. changeHandler = { [ weak self] in
49+ guard let self else { return }
50+ onSave ( self )
51+ }
4552 }
4653
4754 func reset( resetSnapshot: Bool = true ) {
@@ -51,7 +58,7 @@ final class Filespace {
5158 suggestionSourceSnapshot = . init( linesHash: - 1 , cursorPosition: . outOfScope)
5259 }
5360 }
54-
61+
5562 func refreshUpdateTime( ) {
5663 lastSuggestionUpdateTime = Environment . now ( )
5764 }
@@ -71,7 +78,7 @@ final class Workspace {
7178 onChange ? ( )
7279 }
7380 }
74-
81+
7582 struct SuggestionFeatureDisabledError : Error , LocalizedError {
7683 var errorDescription : String ? {
7784 " Suggestion feature is disabled for this project. "
@@ -112,7 +119,7 @@ final class Workspace {
112119 }
113120 return _copilotSuggestionService
114121 }
115-
122+
116123 var isSuggestionFeatureEnabled : Bool {
117124 let isSuggestionDisabledGlobally = UserDefaults . shared
118125 . value ( for: \. disableSuggestionFeatureGlobally)
@@ -127,7 +134,7 @@ final class Workspace {
127134
128135 private init ( projectRootURL: URL ) {
129136 self . projectRootURL = projectRootURL
130-
137+
131138 Task {
132139 userDefaultsObserver. onChange = { [ weak self] in
133140 guard let self else { return }
@@ -140,7 +147,7 @@ final class Workspace {
140147 options: . new,
141148 context: nil
142149 )
143-
150+
144151 UserDefaults . shared. addObserver (
145152 userDefaultsObserver,
146153 forKeyPath: UserDefaultPreferenceKeys ( ) . disableSuggestionFeatureGlobally. key,
@@ -171,22 +178,31 @@ final class Workspace {
171178 return ( workspace, filespace)
172179 }
173180 }
174-
181+
175182 let projectURL = try await Environment . fetchCurrentProjectRootURL ( fileURL)
176183 let workspaceURL = projectURL ?? fileURL
177184 let workspace = workspaces [ workspaceURL] ?? Workspace ( projectRootURL: workspaceURL)
178- let existedFilespace = workspace. filespaces [ fileURL]
179- let filespace = existedFilespace ?? . init( fileURL: fileURL)
180- if workspace. filespaces [ fileURL] == nil {
181- workspace. filespaces [ fileURL] = filespace
182- }
185+ let filespace = workspace. createFilespaceIfNeeded ( fileURL: fileURL)
183186 workspaces [ workspaceURL] = workspace
187+ return ( workspace, filespace)
188+ }
189+
190+ func createFilespaceIfNeeded( fileURL: URL ) -> Filespace {
191+ let existedFilespace = filespaces [ fileURL]
192+ let filespace = existedFilespace ?? . init( fileURL: fileURL, onSave: { [ weak self]
193+ filespace in
194+ guard let self else { return }
195+ notifySaveFile ( filespace: filespace)
196+ } )
197+ if filespaces [ fileURL] == nil {
198+ filespaces [ fileURL] = filespace
199+ }
184200 if existedFilespace == nil {
185- workspace . notifyOpenFile ( filespace: filespace)
201+ notifyOpenFile ( filespace: filespace)
186202 } else {
187203 filespace. refreshUpdateTime ( )
188204 }
189- return ( workspace , filespace)
205+ return filespace
190206 }
191207}
192208
@@ -202,7 +218,8 @@ extension Workspace {
202218 }
203219 lastTriggerDate = Environment . now ( )
204220
205- let filespace = filespaces [ fileURL] ?? . init( fileURL: fileURL)
221+ let filespace = createFilespaceIfNeeded ( fileURL: fileURL)
222+
206223 if filespaces [ fileURL] == nil {
207224 filespaces [ fileURL] = filespace
208225 }
@@ -306,7 +323,7 @@ extension Workspace {
306323
307324 return suggestion
308325 }
309-
326+
310327 func notifyOpenFile( filespace: Filespace ) {
311328 Task {
312329 try await copilotSuggestionService? . notifyOpenTextDocument (
@@ -315,7 +332,7 @@ extension Workspace {
315332 )
316333 }
317334 }
318-
335+
319336 func notifyUpdateFile( filespace: Filespace , content: String ) {
320337 Task {
321338 try await copilotSuggestionService? . notifyChangeTextDocument (
@@ -324,6 +341,12 @@ extension Workspace {
324341 )
325342 }
326343 }
344+
345+ func notifySaveFile( filespace: Filespace ) {
346+ Task {
347+ try await copilotSuggestionService? . notifySaveTextDocument ( fileURL: filespace. fileURL)
348+ }
349+ }
327350}
328351
329352extension Workspace {
@@ -345,3 +368,41 @@ extension Workspace {
345368 realtimeSuggestionRequests = [ ]
346369 }
347370}
371+
372+ final class FileSaveWatcher {
373+ let url : URL
374+ var fileHandle : FileHandle ?
375+ var source : DispatchSourceFileSystemObject ?
376+ var changeHandler : ( ) -> Void = { }
377+
378+ init ( fileURL: URL ) {
379+ url = fileURL
380+ startup ( )
381+ }
382+
383+ deinit {
384+ source? . cancel ( )
385+ }
386+
387+ func startup( ) {
388+ if let source = source {
389+ source. cancel ( )
390+ }
391+
392+ fileHandle = try ? FileHandle ( forReadingFrom: url)
393+ if let fileHandle {
394+ source = DispatchSource . makeFileSystemObjectSource (
395+ fileDescriptor: fileHandle. fileDescriptor,
396+ eventMask: . link,
397+ queue: . main
398+ )
399+
400+ source? . setEventHandler { [ weak self] in
401+ self ? . changeHandler ( )
402+ self ? . startup ( )
403+ }
404+
405+ source? . resume ( )
406+ }
407+ }
408+ }
0 commit comments