Skip to content

Commit ca61d5b

Browse files
committed
Persist real-time suggestion states
1 parent 9db9cc4 commit ca61d5b

File tree

8 files changed

+90
-45
lines changed

8 files changed

+90
-45
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 & 27 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,24 +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 { @ServiceActor in
30-
for (_, workspace) in workspaces {
31-
workspace.isRealtimeSuggestionEnabled = false
32-
}
33-
await AutoTrigger.shared.stop(by: identifier)
34-
}
35-
}
36-
3720
public func checkStatus(withReply reply: @escaping (String?, Error?) -> Void) {
3821
Task { @ServiceActor in
3922
do {
@@ -113,7 +96,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
11396
}
11497
reply(try JSONEncoder().encode(updatedContent), nil)
11598
} catch {
116-
print(error)
99+
os_log(.error, "%@", error.localizedDescription)
117100
reply(nil, NSError.from(error))
118101
}
119102
}
@@ -140,7 +123,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
140123
}
141124
reply(try JSONEncoder().encode(updatedContent), nil)
142125
} catch {
143-
print(error)
126+
os_log(.error, "%@", error.localizedDescription)
144127
reply(nil, NSError.from(error))
145128
}
146129
}
@@ -167,7 +150,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
167150
}
168151
reply(try JSONEncoder().encode(updatedContent), nil)
169152
} catch {
170-
print(error)
153+
os_log(.error, "%@", error.localizedDescription)
171154
reply(nil, NSError.from(error))
172155
}
173156
}
@@ -191,7 +174,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
191174
)
192175
reply(try JSONEncoder().encode(updatedContent), nil)
193176
} catch {
194-
print(error)
177+
os_log(.error, "%@", error.localizedDescription)
195178
reply(nil, NSError.from(error))
196179
}
197180
}
@@ -218,7 +201,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
218201
}
219202
reply(try JSONEncoder().encode(updatedContent), nil)
220203
} catch {
221-
print(error)
204+
os_log(.error, "%@", error.localizedDescription)
222205
reply(nil, NSError.from(error))
223206
}
224207
}
@@ -247,7 +230,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
247230
}
248231
reply(try JSONEncoder().encode(updatedContent), nil)
249232
} catch {
250-
print(error)
233+
os_log(.error, "%@", error.localizedDescription)
251234
reply(nil, NSError.from(error))
252235
}
253236
}
@@ -257,11 +240,19 @@ public class XPCService: NSObject, XPCServiceProtocol {
257240
Task { @ServiceActor in
258241
let fileURL = try await Environment.fetchCurrentFileURL()
259242
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
260-
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+
}
261252
reply(nil)
262253
}
263254
}
264-
255+
265256
public func prefetchRealtimeSuggestions(
266257
editorContent: Data,
267258
withReply reply: @escaping () -> Void
@@ -282,7 +273,7 @@ public class XPCService: NSObject, XPCServiceProtocol {
282273
)
283274
reply()
284275
} catch {
285-
print(error)
276+
os_log(.error, "%@", error.localizedDescription)
286277
reply()
287278
}
288279
}

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
}

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)