Skip to content

Commit 382fd3a

Browse files
committed
Add timer base realtime suggestions
1 parent dbbc439 commit 382fd3a

17 files changed

+358
-47
lines changed

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> {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Foundation
2+
import XPCShared
3+
4+
actor AutoTrigger {
5+
static let shared = AutoTrigger()
6+
7+
var listeners = Set<ObjectIdentifier>()
8+
private var task: Task<Void, Error>?
9+
10+
private init() {}
11+
12+
func start(by listener: ObjectIdentifier) {
13+
listeners.insert(listener)
14+
guard task == nil else { return }
15+
task = Task {
16+
while !Task.isCancelled {
17+
guard UserDefaults.shared.bool(forKey: SettingsKey.isAutoTriggerEnabled) else {
18+
continue
19+
}
20+
try await Task.sleep(nanoseconds: 2_000_000_000)
21+
try? await Environment.triggerAction("Realtime Suggestions")
22+
print("run")
23+
}
24+
}
25+
}
26+
27+
func stop(by listener: ObjectIdentifier) {
28+
listeners.remove(listener)
29+
guard listeners.isEmpty else { return }
30+
task?.cancel()
31+
task = nil
32+
}
33+
}

Core/Sources/Service/Environment.swift

Lines changed: 33 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,39 @@ 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+
let appleScript = """
114+
tell application "System Events"
115+
set proc to item 1 of (processes whose unix id is \(activeXcode.processIdentifier))
116+
tell proc
117+
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
118+
end tell
119+
end tell
120+
"""
121+
122+
try await runAppleScript(appleScript)
106123
}
107124
}
108125

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+
}

Core/Sources/Service/Workspace.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ final class Workspace {
4141
}
4242

4343
var filespaces = [URL: Filespace]()
44+
var isRealtimeSuggestionEnabled = false
4445

4546
private lazy var service: CopilotSuggestionServiceType = Environment
4647
.createSuggestionService(projectRootURL)
@@ -49,6 +50,29 @@ final class Workspace {
4950
self.projectRootURL = projectRootURL
5051
}
5152

53+
/// Trigger only when
54+
/// 1. There is no pending suggestion
55+
/// 2. There are pending suggestions, but either content or cursor is changed, and cursor is not inside of suggestion.
56+
func canAutoTriggerGetSuggestions(
57+
forFileAt fileURL: URL,
58+
content: String,
59+
cursorPosition: CursorPosition
60+
) -> Bool {
61+
guard isRealtimeSuggestionEnabled else { return false }
62+
guard let filespace = filespaces[fileURL] else { return true }
63+
if filespace.suggestions.isEmpty { return true }
64+
if content.hashValue != filespace.latestContentHash { return true }
65+
if cursorPosition != filespace.latestCursorPosition {
66+
if let range = filespace.currentSuggestionLineRange,
67+
range.contains(cursorPosition.line)
68+
{
69+
return false
70+
}
71+
return true
72+
}
73+
return false
74+
}
75+
5276
func getSuggestedCode(
5377
forFileAt fileURL: URL,
5478
content: String,

Core/Sources/Service/XPCService.swift

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,32 @@ public class XPCService: NSObject, XPCServiceProtocol {
1515
@ServiceActor
1616
var workspaces = [URL: Workspace]()
1717

18-
override public init() {}
18+
override public init() {
19+
super.init()
20+
let identifier = ObjectIdentifier(self)
21+
Task {
22+
await AutoTrigger.shared.start(by: identifier)
23+
}
24+
Task { @ServiceActor [weak self] in
25+
while let self, !Task.isCancelled {
26+
try await Task.sleep(nanoseconds: 8 * 60 * 60 * 1_000_000_000)
27+
for (url, workspace) in self.workspaces {
28+
if workspace.isExpired {
29+
self.workspaces[url] = nil
30+
} else {
31+
workspace.cleanUp()
32+
}
33+
}
34+
}
35+
}
36+
}
37+
38+
deinit {
39+
let identifier = ObjectIdentifier(self)
40+
Task {
41+
await AutoTrigger.shared.stop(by: identifier)
42+
}
43+
}
1944

2045
public func checkStatus(withReply reply: @escaping (String?, Error?) -> Void) {
2146
Task { @ServiceActor in
@@ -79,11 +104,9 @@ public class XPCService: NSObject, XPCServiceProtocol {
79104
Task { @ServiceActor in
80105
do {
81106
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
82-
let projectURL = try await Environment.fetchCurrentProjectRootURL()
83107
let fileURL = try await Environment.fetchCurrentFileURL()
84-
let workspaceURL = projectURL ?? fileURL
85-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
86-
workspaces[workspaceURL] = workspace
108+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
109+
87110
let updatedContent = try await workspace.getSuggestedCode(
88111
forFileAt: fileURL,
89112
content: editor.content,
@@ -108,10 +131,9 @@ public class XPCService: NSObject, XPCServiceProtocol {
108131
Task { @ServiceActor in
109132
do {
110133
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
111-
let projectURL = try await Environment.fetchCurrentProjectRootURL()
112134
let fileURL = try await Environment.fetchCurrentFileURL()
113-
let workspaceURL = projectURL ?? fileURL
114-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
135+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
136+
115137
let updatedContent = workspace.getNextSuggestedCode(
116138
forFileAt: fileURL,
117139
content: editor.content,
@@ -133,10 +155,9 @@ public class XPCService: NSObject, XPCServiceProtocol {
133155
Task { @ServiceActor in
134156
do {
135157
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
136-
let projectURL = try await Environment.fetchCurrentProjectRootURL()
137158
let fileURL = try await Environment.fetchCurrentFileURL()
138-
let workspaceURL = projectURL ?? fileURL
139-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
159+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
160+
140161
let updatedContent = workspace.getPreviousSuggestedCode(
141162
forFileAt: fileURL,
142163
content: editor.content,
@@ -158,10 +179,9 @@ public class XPCService: NSObject, XPCServiceProtocol {
158179
Task { @ServiceActor in
159180
do {
160181
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
161-
let projectURL = try await Environment.fetchCurrentProjectRootURL()
162182
let fileURL = try await Environment.fetchCurrentFileURL()
163-
let workspaceURL = projectURL ?? fileURL
164-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
183+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
184+
165185
let updatedContent = workspace.getSuggestionRejectedCode(
166186
forFileAt: fileURL,
167187
content: editor.content,
@@ -183,10 +203,9 @@ public class XPCService: NSObject, XPCServiceProtocol {
183203
Task { @ServiceActor in
184204
do {
185205
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
186-
let projectURL = try await Environment.fetchCurrentProjectRootURL()
187206
let fileURL = try await Environment.fetchCurrentFileURL()
188-
let workspaceURL = projectURL ?? fileURL
189-
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
207+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
208+
190209
let updatedContent = workspace.getSuggestionAcceptedCode(
191210
forFileAt: fileURL,
192211
content: editor.content,
@@ -200,6 +219,63 @@ public class XPCService: NSObject, XPCServiceProtocol {
200219
}
201220
}
202221
}
222+
223+
public func getRealtimeSuggestedCode(
224+
editorContent: Data,
225+
withReply reply: @escaping (Data?, Error?) -> Void
226+
) {
227+
Task { @ServiceActor in
228+
do {
229+
let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent)
230+
let fileURL = try await Environment.fetchCurrentFileURL()
231+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
232+
233+
let canAutoTrigger = workspace.canAutoTriggerGetSuggestions(
234+
forFileAt: fileURL,
235+
content: editor.content,
236+
cursorPosition: editor.cursorPosition
237+
)
238+
guard canAutoTrigger else {
239+
reply(nil, nil)
240+
return
241+
}
242+
print("update")
243+
let updatedContent = try await workspace.getSuggestedCode(
244+
forFileAt: fileURL,
245+
content: editor.content,
246+
lines: editor.lines,
247+
cursorPosition: editor.cursorPosition,
248+
tabSize: editor.tabSize,
249+
indentSize: editor.indentSize,
250+
usesTabsForIndentation: editor.usesTabsForIndentation
251+
)
252+
reply(try JSONEncoder().encode(updatedContent), nil)
253+
} catch {
254+
print(error)
255+
reply(nil, NSError.from(error))
256+
}
257+
}
258+
}
259+
260+
public func setAutoSuggestion(enabled: Bool, withReply reply: @escaping (Error?) -> Void) {
261+
Task { @ServiceActor in
262+
let fileURL = try await Environment.fetchCurrentFileURL()
263+
let workspace = try await fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
264+
workspace.isRealtimeSuggestionEnabled = enabled
265+
reply(nil)
266+
}
267+
}
268+
}
269+
270+
extension XPCService {
271+
@ServiceActor
272+
func fetchOrCreateWorkspaceIfNeeded(fileURL: URL) async throws -> Workspace {
273+
let projectURL = try await Environment.fetchCurrentProjectRootURL()
274+
let workspaceURL = projectURL ?? fileURL
275+
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
276+
workspaces[workspaceURL] = workspace
277+
return workspace
278+
}
203279
}
204280

205281
extension NSError {
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
//
2-
// File.swift
3-
//
4-
//
5-
// Created by Shangxin Guo on 2022/12/8.
6-
//
7-
81
import Foundation
2+
3+
public extension UserDefaults {
4+
static var shared = UserDefaults(suiteName: "5YKZ4Y3DAW.group.com.intii.CopilotForXcode")!
5+
}

Core/Sources/XPCShared/XPCServiceProtocol.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public protocol XPCServiceProtocol {
88
func signInConfirm(userCode: String, withReply reply: @escaping (String?, String?, Error?) -> Void)
99
func signOut(withReply reply: @escaping (String?, Error?) -> Void)
1010
func getVersion(withReply reply: @escaping (String?, Error?) -> Void)
11-
11+
1212
func getSuggestedCode(
1313
editorContent: Data,
1414
withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void
@@ -29,4 +29,10 @@ public protocol XPCServiceProtocol {
2929
editorContent: Data,
3030
withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void
3131
)
32+
func getRealtimeSuggestedCode(
33+
editorContent: Data,
34+
withReply reply: @escaping (Data?, Error?) -> Void
35+
)
36+
37+
func setAutoSuggestion(enabled: Bool, withReply reply: @escaping (Error?) -> Void)
3238
}

0 commit comments

Comments
 (0)