Skip to content

Commit 1f8f4a6

Browse files
committed
Merge branch 'feature/window-base-suggestion-presenter' into develop
2 parents c7b5cbd + e958f38 commit 1f8f4a6

31 files changed

Lines changed: 1557 additions & 531 deletions

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

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

Copilot for Xcode/SettingsView.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ struct SettingsView: View {
99
var realtimeSuggestionToggle: Bool = false
1010
@AppStorage(SettingsKey.realtimeSuggestionDebounce, store: .shared)
1111
var realtimeSuggestionDebounce: Double = 0.7
12+
@AppStorage(SettingsKey.suggestionPresentationMode, store: .shared)
13+
var suggestionPresentationModeRawValue: Int = 0
1214
@State var editingRealtimeSuggestionDebounce: Double = UserDefaults.shared
1315
.value(forKey: SettingsKey.realtimeSuggestionDebounce) as? Double ?? 0.7
1416

@@ -19,6 +21,20 @@ struct SettingsView: View {
1921
Text("Quit service when Xcode and host app are terminated")
2022
}
2123
.toggleStyle(.switch)
24+
25+
Picker(selection: $suggestionPresentationModeRawValue) {
26+
ForEach(PresentationMode.allCases, id: \.rawValue) {
27+
switch $0 {
28+
case .comment:
29+
Text("Comment")
30+
case .floatingWidget:
31+
Text("Floating Widget")
32+
}
33+
}
34+
} label: {
35+
Text("Present suggestions in")
36+
}
37+
2238
Toggle(isOn: $realtimeSuggestionToggle) {
2339
Text("Real-time suggestion")
2440
}

Core/Package.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/ChimeHQ/LanguageClient", from: "0.3.1"),
21+
.package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"),
2122
],
2223
targets: [
2324
.target(name: "CGEventObserver"),
@@ -51,17 +52,50 @@ let package = Package(
5152
),
5253
.target(
5354
name: "Service",
54-
dependencies: ["CopilotModel", "CopilotService", "XPCShared", "CGEventObserver"]
55+
dependencies: [
56+
"CopilotModel",
57+
"CopilotService",
58+
"XPCShared",
59+
"CGEventObserver",
60+
"DisplayLink",
61+
"ActiveApplicationMonitor",
62+
"AXNotificationStream",
63+
"Environment",
64+
"SuggestionWidget",
65+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
66+
]
5567
),
5668
.target(
5769
name: "XPCShared",
5870
dependencies: ["CopilotModel"]
5971
),
6072
.testTarget(
6173
name: "ServiceTests",
62-
dependencies: ["Service", "Client", "CopilotService", "SuggestionInjector", "XPCShared"]
74+
dependencies: [
75+
"Service",
76+
"Client",
77+
"CopilotService",
78+
"SuggestionInjector",
79+
"XPCShared",
80+
"Environment",
81+
]
6382
),
6483
.target(name: "FileChangeChecker"),
6584
.target(name: "LaunchAgentManager"),
85+
.target(name: "DisplayLink"),
86+
.target(name: "ActiveApplicationMonitor"),
87+
.target(name: "AXNotificationStream"),
88+
.target(
89+
name: "Environment",
90+
dependencies: ["ActiveApplicationMonitor", "CopilotService"]
91+
),
92+
.target(
93+
name: "SuggestionWidget",
94+
dependencies: [
95+
"ActiveApplicationMonitor",
96+
"AXNotificationStream",
97+
"Environment",
98+
]
99+
),
66100
]
67101
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import AppKit
2+
import ApplicationServices
3+
import Foundation
4+
5+
public final class AXNotificationStream: AsyncSequence {
6+
public typealias Stream = AsyncStream<Element>
7+
public typealias Continuation = Stream.Continuation
8+
public typealias AsyncIterator = Stream.AsyncIterator
9+
public typealias Element = (name: String, info: CFDictionary)
10+
11+
private var continuation: Continuation
12+
private let stream: Stream
13+
14+
public func makeAsyncIterator() -> Stream.AsyncIterator {
15+
stream.makeAsyncIterator()
16+
}
17+
18+
deinit {
19+
continuation.finish()
20+
}
21+
22+
public init(
23+
app: NSRunningApplication,
24+
element: AXUIElement? = nil,
25+
notificationNames: String...
26+
) {
27+
var cont: Continuation!
28+
stream = Stream { continuation in
29+
cont = continuation
30+
}
31+
continuation = cont
32+
var observer: AXObserver?
33+
34+
func callback(
35+
observer: AXObserver,
36+
element: AXUIElement,
37+
notificationName: CFString,
38+
userInfo: CFDictionary,
39+
pointer: UnsafeMutableRawPointer?
40+
) {
41+
guard let pointer = pointer?.assumingMemoryBound(to: Continuation.self)
42+
else { return }
43+
pointer.pointee.yield((notificationName as String, userInfo))
44+
}
45+
46+
_ = AXObserverCreateWithInfoCallback(
47+
app.processIdentifier,
48+
callback,
49+
&observer
50+
)
51+
guard let observer else {
52+
continuation.finish()
53+
return
54+
}
55+
56+
let observingElement = element ?? AXUIElementCreateApplication(app.processIdentifier)
57+
continuation.onTermination = { @Sendable _ in
58+
for name in notificationNames {
59+
AXObserverRemoveNotification(observer, observingElement, name as CFString)
60+
}
61+
CFRunLoopRemoveSource(
62+
CFRunLoopGetMain(),
63+
AXObserverGetRunLoopSource(observer),
64+
.commonModes
65+
)
66+
}
67+
for name in notificationNames {
68+
AXObserverAddNotification(observer, observingElement, name as CFString, &continuation)
69+
}
70+
CFRunLoopAddSource(
71+
CFRunLoopGetMain(),
72+
AXObserverGetRunLoopSource(observer),
73+
.commonModes
74+
)
75+
}
76+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import AppKit
2+
3+
public final class ActiveApplicationMonitor {
4+
static let shared = ActiveApplicationMonitor()
5+
var activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive)
6+
private var continuations: [UUID: AsyncStream<NSRunningApplication?>.Continuation] = [:]
7+
8+
private init() {
9+
Task {
10+
let sequence = NSWorkspace.shared.notificationCenter
11+
.notifications(named: NSWorkspace.didActivateApplicationNotification)
12+
for await notification in sequence {
13+
guard let app = notification
14+
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
15+
else { continue }
16+
activeApplication = app
17+
notifyContinuations()
18+
}
19+
}
20+
}
21+
22+
deinit {
23+
for continuation in continuations {
24+
continuation.value.finish()
25+
}
26+
}
27+
28+
public static var activeApplication: NSRunningApplication? { shared.activeApplication }
29+
30+
public static var activeXcode: NSRunningApplication? {
31+
if activeApplication?.bundleIdentifier == "com.apple.dt.Xcode" {
32+
return activeApplication
33+
}
34+
return nil
35+
}
36+
37+
public static func createStream() -> AsyncStream<NSRunningApplication?> {
38+
.init { continuation in
39+
let id = UUID()
40+
ActiveApplicationMonitor.shared.addContinuation(continuation, id: id)
41+
continuation.onTermination = { _ in
42+
ActiveApplicationMonitor.shared.removeContinuation(id: id)
43+
}
44+
continuation.yield(activeApplication)
45+
}
46+
}
47+
48+
func addContinuation(
49+
_ continuation: AsyncStream<NSRunningApplication?>.Continuation,
50+
id: UUID
51+
) {
52+
continuations[id] = continuation
53+
}
54+
55+
func removeContinuation(id: UUID) {
56+
continuations[id] = nil
57+
}
58+
59+
private func notifyContinuations() {
60+
for continuation in continuations {
61+
continuation.value.yield(activeApplication)
62+
}
63+
}
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Foundation
2+
import QuartzCore
3+
4+
public actor DisplayLink {
5+
private var displayLink: CVDisplayLink!
6+
private static var _shared = DisplayLink()
7+
static var shared: DisplayLink? {
8+
if let _shared { return _shared }
9+
_shared = DisplayLink()
10+
return _shared
11+
}
12+
private var continuations: [UUID: AsyncStream<Void>.Continuation] = [:]
13+
14+
public static func createStream() -> AsyncStream<Void> {
15+
.init { continuation in
16+
Task {
17+
let id = UUID()
18+
await DisplayLink.shared?.addContinuation(continuation, id: id)
19+
continuation.onTermination = { _ in
20+
Task {
21+
await DisplayLink.shared?.removeContinuation(id: id)
22+
}
23+
}
24+
}
25+
}
26+
}
27+
28+
private init?() {
29+
_ = CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &displayLink)
30+
guard displayLink != nil else { return nil }
31+
CVDisplayLinkSetOutputHandler(displayLink) { [weak self] _, _, _, _, _ in
32+
guard let self else { return kCVReturnSuccess }
33+
Task { await self.notifyContinuations() }
34+
return kCVReturnSuccess
35+
}
36+
}
37+
38+
deinit {
39+
for continuation in continuations {
40+
continuation.value.finish()
41+
}
42+
}
43+
44+
func addContinuation(_ continuation: AsyncStream<Void>.Continuation, id: UUID) {
45+
continuations[id] = continuation
46+
if !continuations.isEmpty {
47+
CVDisplayLinkStart(displayLink)
48+
}
49+
}
50+
51+
func removeContinuation(id: UUID) {
52+
continuations[id] = nil
53+
if continuations.isEmpty {
54+
CVDisplayLinkStop(displayLink)
55+
}
56+
}
57+
58+
private func notifyContinuations() {
59+
for continuation in continuations {
60+
continuation.value.yield()
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)