Skip to content

Commit 58e90f0

Browse files
committed
Merge branch 'feature/tab-to-accept-suggestion' into develop
2 parents 3a25e7a + 4098389 commit 58e90f0

26 files changed

Lines changed: 215 additions & 132 deletions

Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/Package.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ let package = Package(
7171
"SuggestionService",
7272
"GitHubCopilotService",
7373
"XPCShared",
74-
"CGEventObserver",
7574
"DisplayLink",
7675
"SuggestionWidget",
7776
"ChatService",
7877
"PromptToCodeService",
7978
"ServiceUpdateMigration",
8079
"ChatGPTChatTab",
80+
.product(name: "CGEventObserver", package: "Tool"),
8181
.product(name: "Workspace", package: "Tool"),
8282
.product(name: "UserDefaultsObserver", package: "Tool"),
8383
.product(name: "AppMonitoring", package: "Tool"),
@@ -91,7 +91,7 @@ let package = Package(
9191
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
9292
.product(name: "Dependencies", package: "swift-dependencies"),
9393
].pro([
94-
"ProChatTabs",
94+
"ProService",
9595
])
9696
),
9797
.testTarget(
@@ -241,12 +241,6 @@ let package = Package(
241241

242242
// MARK: - Helpers
243243

244-
.target(
245-
name: "CGEventObserver",
246-
dependencies: [
247-
.product(name: "Logger", package: "Tool"),
248-
]
249-
),
250244
.target(name: "FileChangeChecker"),
251245
.target(name: "LaunchAgentManager"),
252246
.target(name: "DisplayLink"),

Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Preferences
22
import SwiftUI
33

4+
#if canImport(ProHostApp)
5+
import ProHostApp
6+
#endif
7+
48
struct SuggestionSettingsView: View {
59
final class Settings: ObservableObject {
610
@AppStorage(\.realtimeSuggestionToggle)
@@ -21,6 +25,8 @@ struct SuggestionSettingsView: View {
2125
var suggestionFeatureProvider
2226
@AppStorage(\.suggestionDisplayCompactMode)
2327
var suggestionDisplayCompactMode
28+
@AppStorage(\.acceptSuggestionWithTab)
29+
var acceptSuggestionWithTab
2430
init() {}
2531
}
2632

@@ -58,8 +64,16 @@ struct SuggestionSettingsView: View {
5864
}
5965

6066
Toggle(isOn: $settings.realtimeSuggestionToggle) {
61-
Text("Real-time suggestion")
67+
Text("Real-time Suggestion")
68+
}
69+
70+
#if canImport(ProHostApp)
71+
WithFeatureEnabled(\.tabToAcceptSuggestion) {
72+
Toggle(isOn: $settings.acceptSuggestionWithTab) {
73+
Text("Accept Suggestion with Tab")
74+
}
6275
}
76+
#endif
6377

6478
HStack {
6579
Toggle(isOn: $settings.disableSuggestionFeatureGlobally) {
@@ -74,7 +88,7 @@ struct SuggestionSettingsView: View {
7488
isOpen: $isSuggestionFeatureEnabledListPickerOpen
7589
)
7690
}
77-
91+
7892
HStack {
7993
Button("Disabled Language List") {
8094
isSuggestionFeatureDisabledLanguageListViewOpen = true
@@ -110,7 +124,7 @@ struct SuggestionSettingsView: View {
110124
Toggle(isOn: $settings.suggestionDisplayCompactMode) {
111125
Text("Hide Buttons")
112126
}
113-
127+
114128
Toggle(isOn: $settings.hideCommonPrecedingSpacesInSuggestion) {
115129
Text("Hide Common Preceding Spaces")
116130
}

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,9 @@ public final class GraphicalUserInterfaceController {
266266
await commandHandler.handleCustomCommand(command)
267267
}
268268
}
269-
269+
}
270+
271+
func start() {
270272
store.send(.start)
271273
}
272274

Core/Sources/Service/GUI/PromptToCodeProvider+Service.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ extension PromptToCodeProvider {
5959
Task {
6060
let handler = PseudoCommandHandler()
6161
await handler.acceptSuggestion()
62-
if let app = ActiveApplicationMonitor.previousActiveApplication, app.isXcode {
62+
if let app = ActiveApplicationMonitor.shared.previousApp, app.isXcode {
6363
try await Task.sleep(nanoseconds: 200_000_000)
6464
app.activate()
6565
}

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class WidgetDataSource {
6060
self?.removePromptToCode(for: url)
6161
let presenter = PresentInWindowSuggestionPresenter()
6262
presenter.closePromptToCode(fileURL: url)
63-
if let app = ActiveApplicationMonitor.previousActiveApplication, app.isXcode {
63+
if let app = ActiveApplicationMonitor.shared.previousApp, app.isXcode {
6464
Task { @MainActor in
6565
try await Task.sleep(nanoseconds: 200_000_000)
6666
app.activate()
@@ -87,11 +87,11 @@ final class WidgetDataSource {
8787

8888
extension WidgetDataSource: SuggestionWidgetDataSource {
8989
func suggestionForFile(at url: URL) async -> SuggestionProvider? {
90-
for workspace in await Service.shared.workspacePool.workspaces.values {
91-
if let filespace = await workspace.filespaces[url],
92-
let suggestion = await filespace.presentingSuggestion
90+
for workspace in Service.shared.workspacePool.workspaces.values {
91+
if let filespace = workspace.filespaces[url],
92+
let suggestion = filespace.presentingSuggestion
9393
{
94-
return await .init(
94+
return .init(
9595
code: suggestion.text,
9696
language: filespace.language,
9797
startLineIndex: suggestion.position.line,
@@ -113,7 +113,7 @@ extension WidgetDataSource: SuggestionWidgetDataSource {
113113
Task {
114114
let handler = PseudoCommandHandler()
115115
await handler.rejectSuggestions()
116-
if let app = ActiveApplicationMonitor.previousActiveApplication,
116+
if let app = ActiveApplicationMonitor.shared.previousApp,
117117
app.isXcode
118118
{
119119
try await Task.sleep(nanoseconds: 200_000_000)
@@ -125,7 +125,7 @@ extension WidgetDataSource: SuggestionWidgetDataSource {
125125
Task {
126126
let handler = PseudoCommandHandler()
127127
await handler.acceptSuggestion()
128-
if let app = ActiveApplicationMonitor.previousActiveApplication,
128+
if let app = ActiveApplicationMonitor.shared.previousApp,
129129
app.isXcode
130130
{
131131
try await Task.sleep(nanoseconds: 200_000_000)

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,8 @@ import QuartzCore
1212
import Workspace
1313
import XcodeInspector
1414

15-
@WorkspaceActor
1615
public class RealtimeSuggestionController {
17-
var eventObserver: CGEventObserverType = CGEventObserver(eventsOfInterest: [
18-
.keyUp,
19-
.keyDown,
20-
.rightMouseDown,
21-
.leftMouseDown,
22-
])
16+
let eventObserver: CGEventObserverType = CGEventObserver(eventsOfInterest: [.keyDown])
2317
private var task: Task<Void, Error>?
2418
private var inflightPrefetchTask: Task<Void, Error>?
2519
private var windowChangeObservationTask: Task<Void, Error>?
@@ -28,34 +22,34 @@ public class RealtimeSuggestionController {
2822
private var focusedUIElement: AXUIElement?
2923
private var sourceEditor: SourceEditor?
3024

31-
init() {
25+
init() {}
26+
27+
func start() {
3228
Task { [weak self] in
33-
if let app = ActiveApplicationMonitor.activeXcode {
29+
if let app = ActiveApplicationMonitor.shared.activeXcode {
3430
self?.handleXcodeChanged(app)
35-
self?.startHIDObservation(by: 1)
31+
self?.startHIDObservation()
3632
}
37-
var previousApp = ActiveApplicationMonitor.activeXcode
38-
for await app in ActiveApplicationMonitor.createStream() {
33+
var previousApp = ActiveApplicationMonitor.shared.activeXcode
34+
for await app in ActiveApplicationMonitor.shared.createStream() {
3935
guard let self else { return }
4036
try Task.checkCancellation()
4137
defer { previousApp = app }
4238

43-
if let app = ActiveApplicationMonitor.activeXcode, app != previousApp {
39+
if let app = ActiveApplicationMonitor.shared.activeXcode, app != previousApp {
4440
self.handleXcodeChanged(app)
4541
}
4642

47-
if ActiveApplicationMonitor.activeXcode != nil {
48-
startHIDObservation(by: 1)
43+
if ActiveApplicationMonitor.shared.activeXcode != nil {
44+
startHIDObservation()
4945
} else {
50-
stopHIDObservation(by: 1)
46+
stopHIDObservation()
5147
}
5248
}
5349
}
5450
}
5551

56-
private func startHIDObservation(by listener: AnyHashable) {
57-
Logger.service.info("Add auto trigger listener: \(listener).")
58-
52+
private func startHIDObservation() {
5953
if task == nil {
6054
task = Task { [weak self, eventObserver] in
6155
for await event in eventObserver.createStream() {
@@ -67,8 +61,7 @@ public class RealtimeSuggestionController {
6761
eventObserver.activateIfPossible()
6862
}
6963

70-
private func stopHIDObservation(by listener: AnyHashable) {
71-
Logger.service.info("Remove auto trigger listener: \(listener).")
64+
private func stopHIDObservation() {
7265
task?.cancel()
7366
task = nil
7467
eventObserver.deactivate()
@@ -98,7 +91,7 @@ public class RealtimeSuggestionController {
9891
}
9992

10093
private func handleFocusElementChange() {
101-
guard let activeXcode = ActiveApplicationMonitor.activeXcode else { return }
94+
guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode else { return }
10295
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
10396
guard let focusElement = application.focusedElement else { return }
10497
let focusElementType = focusElement.description
@@ -233,15 +226,15 @@ public class RealtimeSuggestionController {
233226
/// Looks like the Xcode will keep the panel around until content is changed,
234227
/// not sure how to observe that it's hidden.
235228
func isCompletionPanelPresenting() -> Bool {
236-
guard let activeXcode = ActiveApplicationMonitor.activeXcode else { return false }
229+
guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode else { return false }
237230
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
238231
return application.focusedWindow?.child(identifier: "_XC_COMPLETION_TABLE_") != nil
239232
}
240233

241234
func notifyEditingFileChange(editor: AXUIElement) async {
242235
guard let fileURL = try? await Environment.fetchCurrentFileURL(),
243236
let (workspace, filespace) = try? await Service.shared.workspacePool
244-
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
237+
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
245238
else { return }
246239
workspace.suggestionPlugin?.notifyUpdateFile(filespace: filespace, content: editor.value)
247240
}

Core/Sources/Service/ScheduledCleaner.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public final class ScheduledCleaner {
1616
) {
1717
self.workspacePool = workspacePool
1818
self.guiController = guiController
19+
}
1920

21+
func start() {
2022
// occasionally cleanup workspaces.
2123
Task { @ServiceActor in
2224
while !Task.isCancelled {
@@ -27,7 +29,7 @@ public final class ScheduledCleaner {
2729

2830
// cleanup when Xcode becomes inactive
2931
Task { @ServiceActor in
30-
for await app in ActiveApplicationMonitor.createStream() {
32+
for await app in ActiveApplicationMonitor.shared.createStream() {
3133
try Task.checkCancellation()
3234
if let app, !app.isXcode {
3335
await cleanUp()
@@ -53,21 +55,21 @@ public final class ScheduledCleaner {
5355
}
5456
}
5557
}
56-
for (url, workspace) in await workspacePool.workspaces {
57-
if await workspace.isExpired, workspaceInfos[.url(url)] == nil {
58+
for (url, workspace) in workspacePool.workspaces {
59+
if workspace.isExpired, workspaceInfos[.url(url)] == nil {
5860
Logger.service.info("Remove idle workspace")
59-
for url in await workspace.filespaces.keys {
61+
for url in workspace.filespaces.keys {
6062
await guiController.widgetDataSource.cleanup(for: url)
6163
}
6264
await workspace.cleanUp(availableTabs: [])
63-
await workspacePool.removeWorkspace(url: url)
65+
workspacePool.removeWorkspace(url: url)
6466
} else {
6567
let tabs = (workspaceInfos[.url(url)]?.tabs ?? [])
6668
.union(workspaceInfos[.unknown]?.tabs ?? [])
6769
// cleanup chats for unused files
68-
let filespaces = await workspace.filespaces
70+
let filespaces = workspace.filespaces
6971
for (url, _) in filespaces {
70-
if await workspace.isFilespaceExpired(
72+
if workspace.isFilespaceExpired(
7173
fileURL: url,
7274
availableTabs: tabs
7375
) {
@@ -83,7 +85,7 @@ public final class ScheduledCleaner {
8385

8486
@ServiceActor
8587
public func closeAllChildProcesses() async {
86-
for (_, workspace) in await workspacePool.workspaces {
88+
for (_, workspace) in workspacePool.workspaces {
8789
await workspace.terminateSuggestionService()
8890
}
8991
}

Core/Sources/Service/Service.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import KeyBindingManager
23
import Workspace
34

45
@globalActor public enum ServiceActor {
@@ -18,14 +19,31 @@ public final class Service {
1819
}
1920
return it
2021
}()
22+
2123
@MainActor
2224
public let guiController = GraphicalUserInterfaceController()
23-
@WorkspaceActor
2425
public let realtimeSuggestionController = RealtimeSuggestionController()
2526
public let scheduledCleaner: ScheduledCleaner
27+
let keyBindingManager: KeyBindingManager
2628

2729
private init() {
2830
scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController)
31+
keyBindingManager = .init(
32+
workspacePool: workspacePool,
33+
acceptSuggestion: {
34+
Task {
35+
await PseudoCommandHandler().acceptSuggestion()
36+
}
37+
}
38+
)
39+
}
40+
41+
@MainActor
42+
public func start() {
43+
scheduledCleaner.start()
44+
realtimeSuggestionController.start()
45+
guiController.start()
46+
keyBindingManager.start()
2947
DependencyUpdater().update()
3048
}
3149
}

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ struct PseudoCommandHandler {
168168
}
169169
try await Environment.triggerAction("Accept Suggestion")
170170
} catch {
171-
guard let xcode = ActiveApplicationMonitor.activeXcode ?? ActiveApplicationMonitor
172-
.latestXcode else { return }
171+
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
172+
?? ActiveApplicationMonitor.shared.latestXcode else { return }
173173
let application = AXUIElementCreateApplication(xcode.processIdentifier)
174174
guard let focusElement = application.focusedElement,
175175
focusElement.description == "Source Editor"
@@ -260,8 +260,8 @@ extension PseudoCommandHandler {
260260
cursorPosition: CursorPosition
261261
)?
262262
{
263-
guard let xcode = ActiveApplicationMonitor.activeXcode
264-
?? ActiveApplicationMonitor.latestXcode else { return nil }
263+
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
264+
?? ActiveApplicationMonitor.shared.latestXcode else { return nil }
265265
let application = AXUIElementCreateApplication(xcode.processIdentifier)
266266
guard let focusElement = sourceEditor ?? application.focusedElement,
267267
focusElement.description == "Source Editor"
@@ -282,7 +282,7 @@ extension PseudoCommandHandler {
282282
guard
283283
let fileURL = await getFileURL(),
284284
let (_, filespace) = try? await Service.shared.workspacePool
285-
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
285+
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
286286
else { return nil }
287287
return filespace
288288
}

0 commit comments

Comments
 (0)