@@ -3,6 +3,7 @@ import CopilotModel
33import CopilotService
44import Environment
55import Foundation
6+ import Logger
67import Preferences
78import SuggestionInjector
89import XPCShared
@@ -17,7 +18,7 @@ final class Filespace {
1718 let fileURL : URL
1819 private( set) lazy var language : String = languageIdentifierFromFileURL ( fileURL) . rawValue
1920 var suggestions : [ CopilotCompletion ] = [ ] {
20- didSet { lastSuggestionUpdateTime = Environment . now ( ) }
21+ didSet { refreshUpdateTime ( ) }
2122 }
2223
2324 // stored for pseudo command handler
@@ -39,8 +40,15 @@ final class Filespace {
3940 Environment . now ( ) . timeIntervalSince ( lastSuggestionUpdateTime) > 60 * 60 * 8
4041 }
4142
42- init ( fileURL: URL ) {
43+ let fileSaveWatcher : FileSaveWatcher
44+
45+ fileprivate init ( fileURL: URL , onSave: @escaping ( Filespace ) -> Void ) {
4346 self . fileURL = fileURL
47+ fileSaveWatcher = . init( fileURL: fileURL)
48+ fileSaveWatcher. changeHandler = { [ weak self] in
49+ guard let self else { return }
50+ onSave ( self )
51+ }
4452 }
4553
4654 func reset( resetSnapshot: Bool = true ) {
@@ -50,6 +58,10 @@ final class Filespace {
5058 suggestionSourceSnapshot = . init( linesHash: - 1 , cursorPosition: . outOfScope)
5159 }
5260 }
61+
62+ func refreshUpdateTime( ) {
63+ lastSuggestionUpdateTime = Environment . now ( )
64+ }
5365}
5466
5567@ServiceActor
@@ -66,7 +78,7 @@ final class Workspace {
6678 onChange ? ( )
6779 }
6880 }
69-
81+
7082 struct SuggestionFeatureDisabledError : Error , LocalizedError {
7183 var errorDescription : String ? {
7284 " Suggestion feature is disabled for this project. "
@@ -107,7 +119,7 @@ final class Workspace {
107119 }
108120 return _copilotSuggestionService
109121 }
110-
122+
111123 var isSuggestionFeatureEnabled : Bool {
112124 let isSuggestionDisabledGlobally = UserDefaults . shared
113125 . value ( for: \. disableSuggestionFeatureGlobally)
@@ -122,7 +134,7 @@ final class Workspace {
122134
123135 private init ( projectRootURL: URL ) {
124136 self . projectRootURL = projectRootURL
125-
137+
126138 Task {
127139 userDefaultsObserver. onChange = { [ weak self] in
128140 guard let self else { return }
@@ -135,7 +147,7 @@ final class Workspace {
135147 options: . new,
136148 context: nil
137149 )
138-
150+
139151 UserDefaults . shared. addObserver (
140152 userDefaultsObserver,
141153 forKeyPath: UserDefaultPreferenceKeys ( ) . disableSuggestionFeatureGlobally. key,
@@ -166,17 +178,32 @@ final class Workspace {
166178 return ( workspace, filespace)
167179 }
168180 }
169-
181+
170182 let projectURL = try await Environment . fetchCurrentProjectRootURL ( fileURL)
171183 let workspaceURL = projectURL ?? fileURL
172184 let workspace = workspaces [ workspaceURL] ?? Workspace ( projectRootURL: workspaceURL)
173- let filespace = workspace. filespaces [ fileURL] ?? . init( fileURL: fileURL)
174- if workspace. filespaces [ fileURL] == nil {
175- workspace. filespaces [ fileURL] = filespace
176- }
185+ let filespace = workspace. createFilespaceIfNeeded ( fileURL: fileURL)
177186 workspaces [ workspaceURL] = workspace
178187 return ( workspace, filespace)
179188 }
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+ }
200+ if existedFilespace == nil {
201+ notifyOpenFile ( filespace: filespace)
202+ } else {
203+ filespace. refreshUpdateTime ( )
204+ }
205+ return filespace
206+ }
180207}
181208
182209extension Workspace {
@@ -191,7 +218,8 @@ extension Workspace {
191218 }
192219 lastTriggerDate = Environment . now ( )
193220
194- let filespace = filespaces [ fileURL] ?? . init( fileURL: fileURL)
221+ let filespace = createFilespaceIfNeeded ( fileURL: fileURL)
222+
195223 if filespaces [ fileURL] == nil {
196224 filespaces [ fileURL] = filespace
197225 }
@@ -295,12 +323,39 @@ extension Workspace {
295323
296324 return suggestion
297325 }
326+
327+ func notifyOpenFile( filespace: Filespace ) {
328+ Task {
329+ try await copilotSuggestionService? . notifyOpenTextDocument (
330+ fileURL: filespace. fileURL,
331+ content: try String ( contentsOf: filespace. fileURL, encoding: . utf8)
332+ )
333+ }
334+ }
335+
336+ func notifyUpdateFile( filespace: Filespace , content: String ) {
337+ Task {
338+ try await copilotSuggestionService? . notifyChangeTextDocument (
339+ fileURL: filespace. fileURL,
340+ content: content
341+ )
342+ }
343+ }
344+
345+ func notifySaveFile( filespace: Filespace ) {
346+ Task {
347+ try await copilotSuggestionService? . notifySaveTextDocument ( fileURL: filespace. fileURL)
348+ }
349+ }
298350}
299351
300352extension Workspace {
301353 func cleanUp( ) {
302354 for (fileURL, filespace) in filespaces {
303355 if filespace. isExpired {
356+ Task {
357+ try await copilotSuggestionService? . notifyCloseTextDocument ( fileURL: fileURL)
358+ }
304359 filespaces [ fileURL] = nil
305360 }
306361 }
@@ -313,3 +368,41 @@ extension Workspace {
313368 realtimeSuggestionRequests = [ ]
314369 }
315370}
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