Skip to content

Commit ecb9090

Browse files
committed
Merge branch 'feature/realtime-suggestion' into develop
2 parents dbbc439 + bdcdb69 commit ecb9090

20 files changed

Lines changed: 512 additions & 74 deletions

Core/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
),
1818
],
1919
dependencies: [
20-
.package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.3.1"),
20+
.package(url: "https://github.com/ChimeHQ/LanguageClient", from: "0.3.1"),
2121
],
2222
targets: [
2323
.target(

Core/Sources/Client/AsyncXPCService.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ public struct AsyncXPCService {
119119
{ $0.getSuggestionRejectedCode }
120120
)
121121
}
122+
123+
public func getRealtimeSuggestedCode(editorContent: EditorContent) async throws -> UpdatedContent {
124+
try await suggestionRequest(
125+
connection,
126+
editorContent,
127+
{ $0.getRealtimeSuggestedCode }
128+
)
129+
}
130+
131+
public func setAutoSuggestion(enabled: Bool) async throws {
132+
return try await withXPCServiceConnected(connection: connection) {
133+
service, continuation in
134+
service.setAutoSuggestion(enabled: enabled) { error in
135+
if let error {
136+
continuation.reject(error)
137+
return
138+
}
139+
continuation.resume(())
140+
}
141+
}
142+
}
122143
}
123144

124145
struct AutoFinishContinuation<T> {

Core/Sources/CopilotModel/ExportedFromLSP.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ import LanguageServerProtocol
22

33
public typealias CursorPosition = LanguageServerProtocol.Position
44
public typealias CursorRange = LanguageServerProtocol.LSPRange
5+
6+
public extension CursorPosition {
7+
static var outOfScope: CursorPosition { .init(line: -1, character: -1) }
8+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import XPCShared
3+
4+
actor AutoTrigger {
5+
static let shared = AutoTrigger()
6+
7+
private var listeners = Set<ObjectIdentifier>()
8+
var eventObserver: CGEventObserverType = CGEventObserver()
9+
var task: Task<Void, Error>?
10+
11+
private init() {}
12+
13+
func start(by listener: ObjectIdentifier) {
14+
listeners.insert(listener)
15+
if task == nil {
16+
task = Task { [stream = eventObserver.stream] in
17+
var triggerTask: Task<Void, Error>?
18+
try? await Environment.triggerAction("Realtime Suggestions")
19+
for await _ in stream {
20+
triggerTask?.cancel()
21+
if Task.isCancelled { break }
22+
triggerTask = Task {
23+
try? await Task.sleep(nanoseconds: 2_000_000_000)
24+
if Task.isCancelled { return }
25+
try? await Environment.triggerAction("Realtime Suggestions")
26+
}
27+
}
28+
}
29+
}
30+
eventObserver.activateIfPossible()
31+
}
32+
33+
func stop(by listener: ObjectIdentifier) {
34+
listeners.remove(listener)
35+
guard listeners.isEmpty else { return }
36+
task?.cancel()
37+
task = nil
38+
eventObserver.deactivate()
39+
}
40+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Cocoa
2+
import Foundation
3+
4+
public protocol CGEventObserverType {
5+
@discardableResult
6+
func activateIfPossible() -> Bool
7+
func deactivate()
8+
var stream: AsyncStream<Void> { get }
9+
var isEnabled: Bool { get }
10+
}
11+
12+
final class CGEventObserver: CGEventObserverType {
13+
let stream: AsyncStream<Void>
14+
var isEnabled: Bool { port != nil }
15+
16+
private var continuation: AsyncStream<Void>.Continuation
17+
private var port: CFMachPort?
18+
private let eventsOfInterest: Set<CGEventType> = [.keyUp, .leftMouseUp, .mouseMoved]
19+
private let tapLocation: CGEventTapLocation = .cghidEventTap
20+
private let tapPlacement: CGEventTapPlacement = .tailAppendEventTap
21+
private let tapOptions: CGEventTapOptions = .listenOnly
22+
private var retryTask: Task<Void, Error>?
23+
24+
deinit {
25+
continuation.finish()
26+
CFMachPortInvalidate(port)
27+
}
28+
29+
init() {
30+
var continuation: AsyncStream<Void>.Continuation!
31+
stream = AsyncStream { c in
32+
continuation = c
33+
}
34+
self.continuation = continuation
35+
}
36+
37+
public func deactivate() {
38+
retryTask?.cancel()
39+
retryTask = nil
40+
guard let port = port else { return }
41+
print("Deactivate")
42+
CFMachPortInvalidate(port)
43+
self.port = nil
44+
}
45+
46+
@discardableResult
47+
public func activateIfPossible() -> Bool {
48+
guard AXIsProcessTrusted() else { return false }
49+
guard port == nil else { return true }
50+
51+
let eoi = UInt64(eventsOfInterest.reduce(into: 0) { $0 |= 1 << $1.rawValue })
52+
53+
func callback(
54+
tapProxy _: CGEventTapProxy,
55+
eventType: CGEventType,
56+
event: CGEvent,
57+
continuationPointer: UnsafeMutableRawPointer?
58+
) -> Unmanaged<CGEvent>? {
59+
guard AXIsProcessTrusted() else {
60+
return .passRetained(event)
61+
}
62+
63+
if eventType == .tapDisabledByTimeout || eventType == .tapDisabledByUserInput {
64+
return .passRetained(event)
65+
}
66+
67+
if let continuation = continuationPointer?.assumingMemoryBound(to: AsyncStream<Void>.Continuation.self) {
68+
continuation.pointee.yield(())
69+
}
70+
71+
return .passRetained(event)
72+
}
73+
74+
let tapLocation = self.tapLocation
75+
let tapPlacement = self.tapPlacement
76+
let tapOptions = self.tapOptions
77+
78+
guard let port = withUnsafeMutablePointer(to: &continuation, { pointer in
79+
CGEvent.tapCreate(
80+
tap: tapLocation,
81+
place: tapPlacement,
82+
options: tapOptions,
83+
eventsOfInterest: eoi,
84+
callback: callback,
85+
userInfo: pointer
86+
)
87+
}) else {
88+
retryTask = Task {
89+
try? await Task.sleep(nanoseconds: 2_000_000_000)
90+
try Task.checkCancellation()
91+
activateIfPossible()
92+
}
93+
return false
94+
}
95+
self.port = port
96+
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, port, 0)
97+
CFRunLoopAddSource(RunLoop.main.getCFRunLoop(), runLoopSource, .commonModes)
98+
return true
99+
}
100+
}

Core/Sources/Service/Environment.swift

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import AppKit
2-
import Foundation
32
import CopilotService
3+
import Foundation
44

55
private struct NoAccessToAccessibilityAPIError: Error, LocalizedError {
66
var errorDescription: String? {
77
"Permission not granted to use Accessibility API. Please turn in on in Settings.app."
88
}
99
}
10+
1011
private struct FailedToFetchFileURLError: Error, LocalizedError {
1112
var errorDescription: String? {
1213
"Failed to fetch editing file url."
@@ -23,17 +24,7 @@ enum Environment {
2324
end tell
2425
"""
2526

26-
let task = Process()
27-
task.launchPath = "/usr/bin/osascript"
28-
task.arguments = ["-e", appleScript]
29-
let outpipe = Pipe()
30-
task.standardOutput = outpipe
31-
try task.run()
32-
await Task.yield()
33-
task.waitUntilExit()
34-
if let data = try outpipe.fileHandleForReading.readToEnd(),
35-
let path = String(data: data, encoding: .utf8)
36-
{
27+
if let path = try await runAppleScript(appleScript) {
3728
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
3829
var url = URL(fileURLWithPath: trimmedNewLine)
3930
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
@@ -96,13 +87,46 @@ enum Environment {
9687

9788
throw FailedToFetchFileURLError()
9889
}
99-
90+
10091
static var createAuthService: () -> CopilotAuthServiceType = {
101-
return CopilotAuthService()
92+
CopilotAuthService()
10293
}
103-
94+
10495
static var createSuggestionService: (_ projectRootURL: URL) -> CopilotSuggestionServiceType = { projectRootURL in
105-
return CopilotSuggestionService(projectRootURL: projectRootURL)
96+
CopilotSuggestionService(projectRootURL: projectRootURL)
97+
}
98+
99+
static var triggerAction: (_ name: String) async throws -> Void = { name in
100+
var xcodes = [NSRunningApplication]()
101+
var retryCount = 0
102+
// Sometimes runningApplications returns 0 items.
103+
while xcodes.isEmpty, retryCount < 5 {
104+
xcodes = NSRunningApplication
105+
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
106+
if retryCount > 0 { try await Task.sleep(nanoseconds: 50_000_000) }
107+
retryCount += 1
108+
}
109+
110+
guard let activeXcode = xcodes.first(where: { $0.isActive }) else { return }
111+
let bundleName = Bundle.main.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
112+
113+
/// check if menu is open, if not, click the menu item.
114+
let appleScript = """
115+
tell application "System Events"
116+
set proc to item 1 of (processes whose unix id is \(activeXcode.processIdentifier))
117+
tell proc
118+
repeat with theMenu in menus of menu bar 1
119+
set theValue to value of attribute "AXVisibleChildren" of theMenu
120+
if theValue is not {} then
121+
return
122+
end if
123+
end repeat
124+
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
125+
end tell
126+
end tell
127+
"""
128+
129+
try await runAppleScript(appleScript)
106130
}
107131
}
108132

Core/Sources/Service/Helpers.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,21 @@ extension FileManager {
77
return isDirectory.boolValue
88
}
99
}
10+
11+
@discardableResult
12+
func runAppleScript(_ appleScript: String) async throws -> String? {
13+
let task = Process()
14+
task.launchPath = "/usr/bin/osascript"
15+
task.arguments = ["-e", appleScript]
16+
let outpipe = Pipe()
17+
task.standardOutput = outpipe
18+
try task.run()
19+
await Task.yield()
20+
task.waitUntilExit()
21+
if let data = try outpipe.fileHandleForReading.readToEnd(),
22+
let content = String(data: data, encoding: .utf8)
23+
{
24+
return content
25+
}
26+
return nil
27+
}

0 commit comments

Comments
 (0)