Skip to content

Commit 53deb70

Browse files
committed
Merge branch 'feature/launch-on-demand' into develop
2 parents ac89238 + d6e0166 commit 53deb70

File tree

11 files changed

+173
-67
lines changed

11 files changed

+173
-67
lines changed

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
C83B2B7E293DA0CA00C5ACCD /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C83B2B7D293DA0CA00C5ACCD /* AppInfoView.swift */; };
3131
C83B2B80293DA1B600C5ACCD /* InstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C83B2B7F293DA1B600C5ACCD /* InstructionView.swift */; };
3232
C83B2B82293DC38400C5ACCD /* LaunchAgentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C83B2B81293DC38400C5ACCD /* LaunchAgentView.swift */; };
33+
C841BB242994CAD400B0B336 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C841BB232994CAD400B0B336 /* SettingsView.swift */; };
3334
C8520301293C4D9000460097 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8520300293C4D9000460097 /* Helpers.swift */; };
3435
C8520307293CF13600460097 /* CopilotForXcodeXPCService in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C81458B5293A015C00135263 /* CopilotForXcodeXPCService */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
3536
C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */; };
@@ -180,6 +181,7 @@
180181
C83B2B7D293DA0CA00C5ACCD /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
181182
C83B2B7F293DA1B600C5ACCD /* InstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionView.swift; sourceTree = "<group>"; };
182183
C83B2B81293DC38400C5ACCD /* LaunchAgentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentView.swift; sourceTree = "<group>"; };
184+
C841BB232994CAD400B0B336 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
183185
C848181C293A017300966BB2 /* XPCService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = XPCService.entitlements; sourceTree = "<group>"; };
184186
C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
185187
C8520308293D805800460097 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -315,6 +317,7 @@
315317
C83B2B7B293D9FB400C5ACCD /* CopilotView.swift */,
316318
C83B2B7D293DA0CA00C5ACCD /* AppInfoView.swift */,
317319
C83B2B7F293DA1B600C5ACCD /* InstructionView.swift */,
320+
C841BB232994CAD400B0B336 /* SettingsView.swift */,
318321
C87F3E61293DD004008523E8 /* Styles.swift */,
319322
C8189B1D2938973000C9DCDA /* Assets.xcassets */,
320323
C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */,
@@ -535,6 +538,7 @@
535538
C87F3E62293DD004008523E8 /* Styles.swift in Sources */,
536539
C83B2B82293DC38400C5ACCD /* LaunchAgentView.swift in Sources */,
537540
C83B2B7E293DA0CA00C5ACCD /* AppInfoView.swift in Sources */,
541+
C841BB242994CAD400B0B336 /* SettingsView.swift in Sources */,
538542
C83B2B80293DA1B600C5ACCD /* InstructionView.swift in Sources */,
539543
C83B2B7C293D9FB400C5ACCD /* CopilotView.swift in Sources */,
540544
C83B2B7A293D9C8C00C5ACCD /* LaunchAgentManager.swift in Sources */,

Copilot for Xcode/App.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ struct CopilotForXcodeApp: App {
55
var body: some Scene {
66
WindowGroup {
77
ContentView()
8-
.frame(minWidth: 500, maxWidth: .infinity, minHeight: 700)
8+
.frame(minWidth: 500, minHeight: 700)
9+
.preferredColorScheme(.dark)
910
}
1011
.windowStyle(.hiddenTitleBar)
1112
}

Copilot for Xcode/ContentView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct ContentView: View {
1515
AppInfoView()
1616
LaunchAgentView()
1717
CopilotView()
18+
SettingsView()
1819
InstructionView()
1920
Spacer()
2021
}

Copilot for Xcode/LaunchAgentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ struct LaunchAgentView: View {
2323
}
2424
}
2525
}) {
26-
Text("Set Up Launch Agent for XPC Service")
26+
Text("Set Up Launch Agent")
2727
}
2828
.alert(isPresented: $isDidSetupLaunchAgentAlertPresented) {
2929
.init(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import LaunchAgentManager
2+
import SwiftUI
3+
import XPCShared
4+
5+
struct SettingsView: View {
6+
@AppStorage(SettingsKey.quitXPCServiceOnXcodeAndAppQuit, store: .shared)
7+
var quitXPCServiceOnXcodeAndAppQuit: Bool = false
8+
@AppStorage(SettingsKey.realtimeSuggestionToggle, store: .shared)
9+
var realtimeSuggestionToggle: Bool = false
10+
@AppStorage(SettingsKey.realtimeSuggestionDebounce, store: .shared)
11+
var realtimeSuggestionDebounce: Double = 0.7
12+
@State var editingRealtimeSuggestionDebounce: Double = UserDefaults.shared
13+
.value(forKey: SettingsKey.realtimeSuggestionDebounce) as? Double ?? 0.7
14+
15+
var body: some View {
16+
Section {
17+
Form {
18+
Toggle(isOn: $quitXPCServiceOnXcodeAndAppQuit) {
19+
Text("Quit service when Xcode and host app are terminated")
20+
}
21+
.toggleStyle(.switch)
22+
Toggle(isOn: $realtimeSuggestionToggle) {
23+
Text("Real-time suggestion")
24+
}
25+
.toggleStyle(.switch)
26+
27+
HStack {
28+
Slider(value: $editingRealtimeSuggestionDebounce, in: 0...2, step: 0.1) {
29+
Text("Real-time suggestion fetch debounce")
30+
} onEditingChanged: { _ in
31+
realtimeSuggestionDebounce = editingRealtimeSuggestionDebounce
32+
}
33+
34+
Text(
35+
"\(editingRealtimeSuggestionDebounce.formatted(.number.precision(.fractionLength(2))))s"
36+
)
37+
.font(.body)
38+
.monospacedDigit()
39+
.padding(.vertical, 2)
40+
.padding(.horizontal, 6)
41+
.background(
42+
RoundedRectangle(cornerRadius: 4, style: .continuous)
43+
.fill(Color.white.opacity(0.2))
44+
)
45+
}
46+
}
47+
}.buttonStyle(.copilot)
48+
}
49+
}
50+
51+
struct SettingsView_Preview: PreviewProvider {
52+
static var previews: some View {
53+
SettingsView()
54+
.background(.purple)
55+
}
56+
}

Core/Sources/LaunchAgentManager/LaunchAgentManager.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ public struct LaunchAgentManager {
2424
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2525
<plist version="1.0">
2626
<dict>
27-
<key>RunAtLoad</key>
28-
<true/>
2927
<key>Label</key>
3028
<string>\(serviceIdentifier)</string>
3129
<key>Program</key>

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ public actor RealtimeSuggestionController {
129129
guard shouldTrigger else { return }
130130

131131
inflightPrefetchTask = Task { @ServiceActor in
132-
try? await Task.sleep(nanoseconds: UInt64(
132+
try? await Task.sleep(nanoseconds: UInt64((
133133
UserDefaults.shared
134-
.value(forKey: SettingsKey.realtimeSuggestionDebounce) as? Int
135-
?? 800_000_000
136-
))
134+
.value(forKey: SettingsKey.realtimeSuggestionDebounce) as? Double
135+
?? 0.7
136+
) * 1_000_000_000))
137137
guard UserDefaults.shared.bool(forKey: SettingsKey.realtimeSuggestionToggle)
138138
else { return }
139139
if Task.isCancelled { return }

Core/Sources/XPCShared/UserDefaults.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ public enum SettingsKey {
88
public static let nodePath = "NodePath"
99
public static let realtimeSuggestionToggle = "RealtimeSuggestionToggle"
1010
public static let realtimeSuggestionDebounce = "RealtimeSuggestionDebounce"
11+
public static let quitXPCServiceOnXcodeAndAppQuit = "QuitXPCServiceOnXcodeAndAppQuit"
1112
}

EditorExtension/SourceEditorExtension.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Client
12
import Foundation
23
import XcodeKit
34

@@ -14,6 +15,20 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension {
1415
PrefetchSuggestionsCommand(),
1516
].map(makeCommandDefinition)
1617
}
18+
19+
func extensionDidFinishLaunching() {
20+
#if DEBUG
21+
// In a debug build, we usually want to use the XPC service run from Xcode.
22+
#else
23+
// When the source extension is initialized
24+
// we can call a random command to wake up the XPC service.
25+
Task.detached {
26+
try await Task.sleep(nanoseconds: 1_000_000_000)
27+
let service = try getService()
28+
_ = try await service.checkStatus()
29+
}
30+
#endif
31+
}
1732
}
1833

1934
private let identifierPrefix: String = Bundle.main.bundleIdentifier ?? ""

XPCService/AppDelegate.swift

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import AppKit
2+
import FileChangeChecker
3+
import os.log
24
import Service
35
import ServiceManagement
46
import SwiftUI
@@ -11,9 +13,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
1113
private var statusBarItem: NSStatusItem!
1214

1315
func applicationDidFinishLaunching(_: Notification) {
16+
// setup real-time suggestion controller
17+
_ = RealtimeSuggestionController.shared
18+
setupRestartOnUpdate()
19+
setupQuitOnUserTerminated()
20+
1421
NSApp.setActivationPolicy(.accessory)
1522
buildStatusBarMenu()
16-
AXIsProcessTrustedWithOptions(nil)
1723
}
1824

1925
@objc private func buildStatusBarMenu() {
@@ -66,12 +72,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
6672
break
6773
}
6874
}
69-
UserDefaults.shared.addObserver(
70-
userDefaultsObserver,
71-
forKeyPath: SettingsKey.realtimeSuggestionToggle,
72-
options: .new,
73-
context: nil
74-
)
7575
}
7676

7777
@objc func quit() {
@@ -105,6 +105,70 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
105105
}
106106
UserDefaults.shared.set(isOn, forKey: SettingsKey.realtimeSuggestionToggle)
107107
}
108+
109+
func setupRestartOnUpdate() {
110+
Task {
111+
guard let url = Bundle.main.executableURL else { return }
112+
let checker = await FileChangeChecker(fileURL: url)
113+
114+
// If Xcode or Copilot for Xcode is made active, check if the executable of this program
115+
// is changed. If changed, restart the launch agent.
116+
117+
let sequence = NSWorkspace.shared.notificationCenter
118+
.notifications(named: NSWorkspace.didActivateApplicationNotification)
119+
for await notification in sequence {
120+
try Task.checkCancellation()
121+
guard let app = notification
122+
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
123+
app.isUserOfService
124+
else { continue }
125+
guard await checker.checkIfChanged() else {
126+
os_log(.info, "XPC Service is not updated, no need to restart.")
127+
continue
128+
}
129+
os_log(.info, "XPC Service will be restarted.")
130+
#if DEBUG
131+
#else
132+
let manager = LaunchAgentManager(
133+
serviceIdentifier: serviceIdentifier,
134+
executablePath: Bundle.main.executablePath ?? ""
135+
)
136+
do {
137+
try await manager.restartLaunchAgent()
138+
} catch {
139+
os_log(
140+
.error,
141+
"XPC Service failed to restart. %{public}s",
142+
error.localizedDescription
143+
)
144+
}
145+
#endif
146+
}
147+
}
148+
}
149+
150+
func setupQuitOnUserTerminated() {
151+
Task {
152+
// Whenever Xcode or the host application quits, check if any of the two is running.
153+
// If none, quit the XPC service.
154+
155+
let sequence = NSWorkspace.shared.notificationCenter
156+
.notifications(named: NSWorkspace.didTerminateApplicationNotification)
157+
for await notification in sequence {
158+
try Task.checkCancellation()
159+
guard UserDefaults.shared.bool(forKey: SettingsKey.quitXPCServiceOnXcodeAndAppQuit)
160+
else { continue }
161+
guard let app = notification
162+
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
163+
app.isUserOfService
164+
else { continue }
165+
if NSWorkspace.shared.runningApplications.contains(where: \.isUserOfService) {
166+
continue
167+
}
168+
exit(0)
169+
}
170+
}
171+
}
108172
}
109173

110174
private class UserDefaultsObserver: NSObject {
@@ -118,4 +182,22 @@ private class UserDefaultsObserver: NSObject {
118182
) {
119183
onChange?(keyPath)
120184
}
185+
186+
override init() {
187+
super.init()
188+
observe(keyPath: SettingsKey.realtimeSuggestionToggle)
189+
}
190+
191+
func observe(keyPath: String) {
192+
UserDefaults.shared.addObserver(self, forKeyPath: keyPath, options: .new, context: nil)
193+
}
194+
}
195+
196+
extension NSRunningApplication {
197+
var isUserOfService: Bool {
198+
[
199+
"com.apple.dt.Xcode",
200+
bundleIdentifierBase,
201+
].contains(bundleIdentifier)
202+
}
121203
}

0 commit comments

Comments
 (0)