Skip to content

Commit 06811a9

Browse files
committed
Merge branch 'feature/open-file-aware-suggestion' into develop
2 parents 9317f66 + 4493d45 commit 06811a9

5 files changed

Lines changed: 176 additions & 17 deletions

File tree

Core/Sources/CopilotService/CopilotService.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CopilotModel
22
import Foundation
33
import LanguageClient
44
import LanguageServerProtocol
5+
import Logger
56
import Preferences
67
import XPCShared
78

@@ -25,10 +26,15 @@ public protocol CopilotSuggestionServiceType {
2526
) async throws -> [CopilotCompletion]
2627
func notifyAccepted(_ completion: CopilotCompletion) async
2728
func notifyRejected(_ completions: [CopilotCompletion]) async
29+
func notifyOpenTextDocument(fileURL: URL, content: String) async throws
30+
func notifyChangeTextDocument(fileURL: URL, content: String) async throws
31+
func notifyCloseTextDocument(fileURL: URL) async throws
32+
func notifySaveTextDocument(fileURL: URL) async throws
2833
}
2934

3035
protocol CopilotLSP {
3136
func sendRequest<E: CopilotRequestType>(_ endpoint: E) async throws -> E.Response
37+
func sendNotification(_ notif: ClientNotification) async throws
3238
}
3339

3440
public class CopilotBaseService {
@@ -57,7 +63,7 @@ public class CopilotBaseService {
5763
}
5864
let executionParams: Process.ExecutionParameters
5965
let runner = UserDefaults.shared.value(for: \.runNodeWith)
60-
66+
6167
switch runner {
6268
case .bash:
6369
let nodePath = UserDefaults.shared.value(for: \.nodePath)
@@ -248,6 +254,57 @@ public final class CopilotSuggestionService: CopilotBaseService, CopilotSuggesti
248254
CopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.uuid))
249255
)
250256
}
257+
258+
public func notifyOpenTextDocument(
259+
fileURL: URL,
260+
content: String
261+
) async throws {
262+
let languageId = languageIdentifierFromFileURL(fileURL)
263+
let uri = "file://\(fileURL.path)"
264+
// Logger.service.debug("Open \(uri)")
265+
try await server.sendNotification(
266+
.didOpenTextDocument(
267+
DidOpenTextDocumentParams(
268+
textDocument: .init(
269+
uri: uri,
270+
languageId: languageId.rawValue,
271+
version: 0,
272+
text: content
273+
)
274+
)
275+
)
276+
)
277+
}
278+
279+
public func notifyChangeTextDocument(fileURL: URL, content: String) async throws {
280+
let uri = "file://\(fileURL.path)"
281+
// Logger.service.debug("Change \(uri)")
282+
try await server.sendNotification(
283+
.didChangeTextDocument(
284+
DidChangeTextDocumentParams(
285+
uri: uri,
286+
version: 0,
287+
contentChange: .init(
288+
range: nil,
289+
rangeLength: nil,
290+
text: content
291+
)
292+
)
293+
)
294+
)
295+
}
296+
297+
public func notifySaveTextDocument(fileURL: URL) async throws {
298+
let uri = "file://\(fileURL.path)"
299+
// Logger.service.debug("Save \(uri)")
300+
try await server.sendNotification(.didSaveTextDocument(.init(uri: uri)))
301+
}
302+
303+
public func notifyCloseTextDocument(fileURL: URL) async throws {
304+
let uri = "file://\(fileURL.path)"
305+
// Logger.service.debug("Close \(uri)")
306+
try await server.sendNotification(.didCloseTextDocument(.init(uri: uri)))
307+
}
251308
}
252309

253310
extension InitializingServer: CopilotLSP {

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public class RealtimeSuggestionController {
131131
switch notification.name {
132132
case kAXValueChangedNotification:
133133
self.triggerPrefetchDebounced()
134+
await self.notifyEditingFileChange(editor: focusElement)
134135
default:
135136
continue
136137
}
@@ -207,7 +208,7 @@ public class RealtimeSuggestionController {
207208

208209
guard UserDefaults.shared.value(for: \.realtimeSuggestionToggle)
209210
else { return }
210-
211+
211212
if UserDefaults.shared.value(for: \.disableSuggestionFeatureGlobally),
212213
let fileURL = try? await Environment.fetchCurrentFileURL(),
213214
let (workspace, _) = try? await Workspace
@@ -216,7 +217,7 @@ public class RealtimeSuggestionController {
216217
let isEnabled = workspace.isSuggestionFeatureEnabled
217218
if !isEnabled { return }
218219
}
219-
220+
220221
if Task.isCancelled { return }
221222

222223
Logger.service.info("Prefetch suggestions.")
@@ -264,4 +265,12 @@ public class RealtimeSuggestionController {
264265
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
265266
return application.focusedWindow?.child(identifier: "_XC_COMPLETION_TABLE_") != nil
266267
}
268+
269+
func notifyEditingFileChange(editor: AXUIElement) async {
270+
guard let fileURL = try? await Environment.fetchCurrentFileURL(),
271+
let (workspace, filespace) = try? await Workspace
272+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
273+
else { return }
274+
workspace.notifyUpdateFile(filespace: filespace, content: editor.value)
275+
}
267276
}

Core/Sources/Service/ScheduledCleaner.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public final class ScheduledCleaner {
55
// occasionally cleanup workspaces.
66
Task { @ServiceActor in
77
while !Task.isCancelled {
8-
try await Task.sleep(nanoseconds: 8 * 60 * 60 * 1_000_000_000)
8+
try await Task.sleep(nanoseconds: 2 * 60 * 60 * 1_000_000_000)
99
for (url, workspace) in workspaces {
1010
if workspace.isExpired {
1111
workspaces[url] = nil

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
147147
defer { presenter.markAsProcessing(false) }
148148

149149
let fileURL = try await Environment.fetchCurrentFileURL()
150-
let (workspace, _) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
150+
let (workspace, filespace) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
151151

152152
let injector = SuggestionInjector()
153153
var lines = editor.lines

Core/Sources/Service/Workspace.swift

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import CopilotModel
33
import CopilotService
44
import Environment
55
import Foundation
6+
import Logger
67
import Preferences
78
import SuggestionInjector
89
import 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

182209
extension 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

300352
extension 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

Comments
 (0)