Skip to content

Commit f5cca40

Browse files
committed
Merge branch 'feature/persist-real-time-suggestions-state' into develop
2 parents fe5aa78 + 93f08bd commit f5cca40

File tree

9 files changed

+97
-48
lines changed

9 files changed

+97
-48
lines changed

Core/Sources/Client/XPCService.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import XPCShared
3+
import os.log
34

45
var asyncService: AsyncXPCService?
56
var shared = XPCService()
@@ -30,11 +31,11 @@ class XPCService {
3031
connection.remoteObjectInterface =
3132
NSXPCInterface(with: XPCServiceProtocol.self)
3233
connection.invalidationHandler = { [weak self] in
33-
print("XPCService Invalidated")
34+
os_log(.info, "XPCService Invalidated")
3435
self?.isInvalidated = true
3536
}
3637
connection.interruptionHandler = { [weak self] in
37-
print("XPCService interrupted")
38+
os_log(.info, "XPCService interrupted")
3839
}
3940
connection.resume()
4041
return connection

Core/Sources/Service/AutoTrigger.swift

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import AppKit
12
import Foundation
23
import XPCShared
4+
import os.log
35

4-
actor AutoTrigger {
5-
static let shared = AutoTrigger()
6+
public actor AutoTrigger {
7+
public static let shared = AutoTrigger()
68

7-
private var listeners = Set<ObjectIdentifier>()
9+
private var listeners = Set<AnyHashable>()
810
var eventObserver: CGEventObserverType = CGEventObserver()
911
var task: Task<Void, Error>?
1012

1113
private init() {
14+
// Occasionally cleanup workspaces.
1215
Task { @ServiceActor in
1316
while !Task.isCancelled {
1417
try await Task.sleep(nanoseconds: 8 * 60 * 60 * 1_000_000_000)
@@ -21,15 +24,42 @@ actor AutoTrigger {
2124
}
2225
}
2326
}
27+
28+
// Start the auto trigger if Xcode is running.
29+
Task {
30+
for xcode in await Environment.runningXcodes() {
31+
await start(by: xcode.processIdentifier)
32+
}
33+
let sequence = NSWorkspace.shared.notificationCenter
34+
.notifications(named: NSWorkspace.didLaunchApplicationNotification)
35+
for await notification in sequence {
36+
try Task.checkCancellation()
37+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { continue }
38+
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
39+
await start(by: app.processIdentifier)
40+
}
41+
}
42+
43+
// Remove listener if Xcode is terminated.
44+
Task {
45+
let sequence = NSWorkspace.shared.notificationCenter
46+
.notifications(named: NSWorkspace.didTerminateApplicationNotification)
47+
for await notification in sequence {
48+
try Task.checkCancellation()
49+
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { continue }
50+
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
51+
await stop(by: app.processIdentifier)
52+
}
53+
}
2454
}
2555

26-
func start(by listener: ObjectIdentifier) {
56+
func start(by listener: AnyHashable) {
57+
os_log(.info, "Add auto trigger listener: %@.", listener as CVarArg)
2758
listeners.insert(listener)
2859

2960
if task == nil {
3061
task = Task { [stream = eventObserver.stream] in
3162
var triggerTask: Task<Void, Error>?
32-
try? await Environment.triggerAction("Prefetch Suggestions")
3363
for await _ in stream {
3464
triggerTask?.cancel()
3565
if Task.isCancelled { break }
@@ -47,10 +77,11 @@ actor AutoTrigger {
4777
try? await Task.sleep(nanoseconds: 2_000_000_000)
4878
if Task.isCancelled { return }
4979
let fileURL = try? await Environment.fetchCurrentFileURL()
50-
guard let folderURL = try? await Environment.fetchCurrentProjectRootURL(fileURL),
51-
let workspace = workspaces[folderURL],
52-
workspace.isRealtimeSuggestionEnabled
80+
guard let folderURL = try? await Environment.fetchCurrentProjectRootURL(fileURL)
5381
else { return }
82+
let workspace = workspaces[folderURL] ?? Workspace(projectRootURL: folderURL)
83+
workspaces[folderURL] = workspace
84+
guard workspace.isRealtimeSuggestionEnabled else { return }
5485
if Task.isCancelled { return }
5586
try? await Environment.triggerAction("Prefetch Suggestions")
5687
}
@@ -60,9 +91,11 @@ actor AutoTrigger {
6091
eventObserver.activateIfPossible()
6192
}
6293

63-
func stop(by listener: ObjectIdentifier) {
94+
func stop(by listener: AnyHashable) {
95+
os_log(.info, "Remove auto trigger listener: %@.", listener as CVarArg)
6496
listeners.remove(listener)
6597
guard listeners.isEmpty else { return }
98+
os_log(.info, "Auto trigger is stopped.")
6699
task?.cancel()
67100
task = nil
68101
eventObserver.deactivate()

Core/Sources/Service/CGEventObserver.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Cocoa
22
import Foundation
3+
import os.log
34

45
public protocol CGEventObserverType {
56
@discardableResult
@@ -38,7 +39,7 @@ final class CGEventObserver: CGEventObserverType {
3839
retryTask?.cancel()
3940
retryTask = nil
4041
guard let port = port else { return }
41-
print("Deactivate")
42+
os_log(.info, "CGEventObserver deactivated.")
4243
CFMachPortInvalidate(port)
4344
self.port = nil
4445
}
@@ -95,6 +96,7 @@ final class CGEventObserver: CGEventObserverType {
9596
self.port = port
9697
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, port, 0)
9798
CFRunLoopAddSource(RunLoop.main.getCFRunLoop(), runLoopSource, .commonModes)
99+
os_log(.info, "CGEventObserver activated.")
98100
return true
99101
}
100102
}

Core/Sources/Service/Environment.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ private struct FailedToFetchFileURLError: Error, LocalizedError {
1717
enum Environment {
1818
static var now = { Date() }
1919

20+
static var runningXcodes: () async -> [NSRunningApplication] = {
21+
var xcodes = [NSRunningApplication]()
22+
var retryCount = 0
23+
// Sometimes runningApplications returns 0 items.
24+
while xcodes.isEmpty, retryCount < 3 {
25+
xcodes = NSRunningApplication
26+
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
27+
try? await Task.sleep(nanoseconds: 1_000_000)
28+
retryCount += 1
29+
}
30+
return xcodes
31+
}
32+
2033
static var isXcodeActive: () async -> Bool = {
2134
var activeXcodes = [NSRunningApplication]()
2235
var retryCount = 0
2336
// Sometimes runningApplications returns 0 items.
2437
while activeXcodes.isEmpty, retryCount < 3 {
2538
activeXcodes = NSRunningApplication
2639
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
27-
.sorted { lhs, _ in
28-
if lhs.isActive { return true }
29-
return false
30-
}
31-
if retryCount > 0 { try? await Task.sleep(nanoseconds: 1_000_000) }
40+
.filter { $0.isActive }
41+
try? await Task.sleep(nanoseconds: 1_000_000)
3242
retryCount += 1
3343
}
3444
return !activeXcodes.isEmpty

Core/Sources/Service/Workspace.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ final class Workspace {
4545
}
4646

4747
var filespaces = [URL: Filespace]()
48-
var isRealtimeSuggestionEnabled = false
48+
var isRealtimeSuggestionEnabled: Bool {
49+
(UserDefaults.shared.dictionary(
50+
forKey: SettingsKey.realtimeSuggestionState
51+
)?[projectRootURL.absoluteString]) as? Bool ?? false
52+
}
53+
4954
var realtimeSuggestionRequests = Set<Task<Void, Error>>()
5055

5156
private lazy var service: CopilotSuggestionServiceType = Environment

Core/Sources/Service/XPCService.swift

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AppKit
22
import CopilotService
33
import Foundation
44
import LanguageServerProtocol
5+
import os.log
56
import XPCShared
67

78
@globalActor enum ServiceActor {
@@ -16,21 +17,6 @@ public class XPCService: NSObject, XPCServiceProtocol {
1617
@ServiceActor
1718
lazy var authService: CopilotAuthServiceType = Environment.createAuthService()
1819

19-
override public init() {
20-
super.init()
21-
let identifier = ObjectIdentifier(self)
22-
Task {
23-
await AutoTrigger.shared.start(by: identifier)
24-
}
25-
}
26-
27-
deinit {
28-
let identifier = ObjectIdentifier(self)
29-
Task {
30-
await AutoTrigger.shared.stop(by: identifier)
31-
}
32-
}
33-
3420
public func checkStatus(withReply reply: @escaping (String?, Error?) -> Void) {
3521
Task { @ServiceActor in
3622
do {
@@ -110,7 +96,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
11096
}
11197
reply(try JSONEncoder().encode(updatedContent), nil)
11298
} catch {
113-
print(error)
99+
os_log(.error, "%@", error.localizedDescription)
114100
reply(nil, NSError.from(error))
115101
}
116102
}
@@ -137,7 +123,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
137123
}
138124
reply(try JSONEncoder().encode(updatedContent), nil)
139125
} catch {
140-
print(error)
126+
os_log(.error, "%@", error.localizedDescription)
141127
reply(nil, NSError.from(error))
142128
}
143129
}
@@ -164,7 +150,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
164150
}
165151
reply(try JSONEncoder().encode(updatedContent), nil)
166152
} catch {
167-
print(error)
153+
os_log(.error, "%@", error.localizedDescription)
168154
reply(nil, NSError.from(error))
169155
}
170156
}
@@ -188,7 +174,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
188174
)
189175
reply(try JSONEncoder().encode(updatedContent), nil)
190176
} catch {
191-
print(error)
177+
os_log(.error, "%@", error.localizedDescription)
192178
reply(nil, NSError.from(error))
193179
}
194180
}
@@ -215,7 +201,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
215201
}
216202
reply(try JSONEncoder().encode(updatedContent), nil)
217203
} catch {
218-
print(error)
204+
os_log(.error, "%@", error.localizedDescription)
219205
reply(nil, NSError.from(error))
220206
}
221207
}
@@ -244,7 +230,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
244230
}
245231
reply(try JSONEncoder().encode(updatedContent), nil)
246232
} catch {
247-
print(error)
233+
os_log(.error, "%@", error.localizedDescription)
248234
reply(nil, NSError.from(error))
249235
}
250236
}
@@ -254,11 +240,19 @@ public class XPCService: NSObject, XPCServiceProtocol {
254240
Task { @ServiceActor in
255241
let fileURL = try await Environment.fetchCurrentFileURL()
256242
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
257-
workspace.isRealtimeSuggestionEnabled = enabled
243+
if var state = UserDefaults.shared.dictionary(forKey: SettingsKey.realtimeSuggestionState) {
244+
state[workspace.projectRootURL.absoluteString] = enabled
245+
UserDefaults.shared.set(state, forKey: SettingsKey.realtimeSuggestionState)
246+
} else {
247+
UserDefaults.shared.set(
248+
[workspace.projectRootURL.absoluteString: enabled],
249+
forKey: SettingsKey.realtimeSuggestionState
250+
)
251+
}
258252
reply(nil)
259253
}
260254
}
261-
255+
262256
public func prefetchRealtimeSuggestions(
263257
editorContent: Data,
264258
withReply reply: @escaping () -> Void
@@ -279,7 +273,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
279273
)
280274
reply()
281275
} catch {
282-
print(error)
276+
os_log(.error, "%@", error.localizedDescription)
283277
reply()
284278
}
285279
}

Core/Sources/XPCShared/UserDefaults.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public extension UserDefaults {
66

77
public enum SettingsKey {
88
public static let nodePath = "NodePath"
9+
public static let realtimeSuggestionState = "RealtimeSuggestionState"
910
}

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,23 @@ The first time the commands run, the extension will ask for 2 types of permissio
3838
- Previous Suggestion: If there is more than 1 suggestion, switch to the previous one.
3939
- Accept Suggestion: Add the suggestion to the code.
4040
- Reject Suggestion: Remove the suggestion comments.
41-
- Turn On Real-time Suggestions: When turn on, Copilot will auto-insert suggestion comments to your code while editing. You have to manually turn it on for every open window of Xcode.
41+
- Turn On Real-time Suggestions: When turn on, Copilot will auto-insert suggestion comments to your code while editing.
4242
- Turn Off Real-time Suggestions: Turns the real-time suggestions off.
4343
- Real-time Suggestions: It is an entry point only for Copilot for Xcode. When suggestions are successfully fetched, Copilot for Xcode will run this command to present the suggestions.
4444
- Prefetch Suggestions: It is an entry point only for Copilot for Xcode. In the background, Copilot for Xcode will occasionally run this command to prefetch real-time suggestions.
4545

4646
**About real-time suggestions**
4747

48-
The implementation won't feel as smooth as that of VSCode.
48+
- The on/off state is persisted, make sure you turn it off manually if you no longer want it.
49+
- The implementation won't feel as smooth as that of VSCode.
50+
51+
The magic behind it is that it will keep calling the command from the menu when you are not typing or clicking the mouse. So it will have to listen to those events, I am not sure if people like it.
4952

50-
The magic behind it is that it will keep calling the command from the menu when you are not typing, or clicking mouse. So it will have to listen to those events, I am not sure if people like it.
51-
52-
Hope that next year, Apple can spend some time on Xcode Extensions.
53+
Hope that next year, Apple can spend some time on Xcode Extensions.
5354

5455
## Prevent Suggestions Being Committed
5556

56-
Since the suggestions are presented as comments, they are in your code. If you are not careful enough, they can be committed to your git repo. To avoid that, I would recommend adding a pre-commit git hook to prevent this from happening. Maybe later I will add an command for that.
57+
Since the suggestions are presented as comments, they are in your code. If you are not careful enough, they can be committed to your git repo. To avoid that, I would recommend adding a pre-commit git hook to prevent this from happening.
5758

5859
```sh
5960
#!/bin/sh

XPCService/main.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Service
23

34
let listener = NSXPCListener(
45
machServiceName: Bundle.main.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String
@@ -7,4 +8,5 @@ let listener = NSXPCListener(
78
let delegate = ServiceDelegate()
89
listener.delegate = delegate
910
listener.resume()
11+
_ = AutoTrigger.shared
1012
RunLoop.main.run()

0 commit comments

Comments
 (0)