Skip to content

Commit 58bf5d4

Browse files
committed
Improve the implementation of realtime suggestions
1 parent b75c318 commit 58bf5d4

5 files changed

Lines changed: 222 additions & 107 deletions

File tree

Core/Sources/Service/AutoTrigger.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ actor AutoTrigger {
3333
triggerTask?.cancel()
3434
if Task.isCancelled { break }
3535
triggerTask = Task { @ServiceActor in
36-
try? await Task.sleep(nanoseconds: 2_000_000_000)
36+
try? await Task.sleep(nanoseconds: 3_000_000_000)
3737
if Task.isCancelled { return }
3838
let fileURL = try? await Environment.fetchCurrentFileURL()
3939
guard let folderURL = try? await Environment.fetchCurrentProjectRootURL(fileURL),

Core/Sources/Service/Environment.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ enum Environment {
2424
end tell
2525
"""
2626

27-
if let path = try await runAppleScript(appleScript) {
27+
let path = try await runAppleScript(appleScript)
28+
if !path.isEmpty {
2829
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
2930
var url = URL(fileURLWithPath: trimmedNewLine)
3031
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
@@ -61,7 +62,7 @@ enum Environment {
6162
if lhs.isActive { return true }
6263
return false
6364
}
64-
if retryCount > 0 { try await Task.sleep(nanoseconds: 50_000_000) }
65+
if retryCount > 0 { try await Task.sleep(nanoseconds: 10_000_000) }
6566
retryCount += 1
6667
}
6768

@@ -117,7 +118,7 @@ enum Environment {
117118
while xcodes.isEmpty, retryCount < 5 {
118119
xcodes = NSRunningApplication
119120
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
120-
if retryCount > 0 { try await Task.sleep(nanoseconds: 50_000_000) }
121+
if retryCount > 0 { try await Task.sleep(nanoseconds: 10_000_000) }
121122
retryCount += 1
122123
}
123124

Core/Sources/Service/Helpers.swift

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import LanguageServerProtocol
23

34
extension FileManager {
45
func fileIsDirectory(atPath path: String) -> Bool {
@@ -9,19 +10,86 @@ extension FileManager {
910
}
1011

1112
@discardableResult
12-
func runAppleScript(_ appleScript: String) async throws -> String? {
13+
func runAppleScript(_ appleScript: String) async throws -> String {
1314
let task = Process()
1415
task.launchPath = "/usr/bin/osascript"
1516
task.arguments = ["-e", appleScript]
1617
let outpipe = Pipe()
1718
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
19+
20+
return try await withUnsafeThrowingContinuation { continuation in
21+
do {
22+
task.terminationHandler = { _ in
23+
do {
24+
if let data = try outpipe.fileHandleForReading.readToEnd(),
25+
let content = String(data: data, encoding: .utf8)
26+
{
27+
continuation.resume(returning: content)
28+
return
29+
}
30+
continuation.resume(returning: "")
31+
} catch {
32+
continuation.resume(throwing: error)
33+
}
34+
}
35+
try task.run()
36+
} catch {
37+
continuation.resume(throwing: error)
38+
}
39+
}
40+
}
41+
42+
extension XPCService {
43+
@ServiceActor
44+
func fetchOrCreateWorkspaceIfNeeded(fileURL: URL) async throws -> Workspace {
45+
let projectURL = try await Environment.fetchCurrentProjectRootURL(fileURL)
46+
let workspaceURL = projectURL ?? fileURL
47+
let workspace = workspaces[workspaceURL] ?? Workspace(projectRootURL: workspaceURL)
48+
workspaces[workspaceURL] = workspace
49+
return workspace
50+
}
51+
}
52+
53+
extension NSError {
54+
static func from(_ error: Error) -> NSError {
55+
if let error = error as? ServerError {
56+
var message = "Unknown"
57+
switch error {
58+
case let .handlerUnavailable(handler):
59+
message = "Handler unavailable: \(handler)."
60+
case let .unhandledMethod(method):
61+
message = "Methond unhandled: \(method)."
62+
case let .notificationDispatchFailed(error):
63+
message = "Notification dispatch failed: \(error.localizedDescription)."
64+
case let .requestDispatchFailed(error):
65+
message = "Request dispatch failed: \(error.localizedDescription)."
66+
case let .clientDataUnavailable(error):
67+
message = "Client data unavalable: \(error.localizedDescription)."
68+
case .serverUnavailable:
69+
message = "Server unavailable, please make sure you have installed Node."
70+
case .missingExpectedParameter:
71+
message = "Missing expected parameter."
72+
case .missingExpectedResult:
73+
message = "Missing expected result."
74+
case let .unableToDecodeRequest(error):
75+
message = "Unable to decode request: \(error.localizedDescription)."
76+
case let .unableToSendRequest(error):
77+
message = "Unable to send request: \(error.localizedDescription)."
78+
case let .unableToSendNotification(error):
79+
message = "Unable to send notification: \(error.localizedDescription)."
80+
case let .serverError(code, m, _):
81+
message = "Server error: (\(code)) \(m)."
82+
case let .invalidRequest(error):
83+
message = "Invalid request: \(error?.localizedDescription ?? "Unknown")."
84+
case .timeout:
85+
message = "Timeout."
86+
}
87+
return NSError(domain: "com.intii.CopilotForXcode", code: -1, userInfo: [
88+
NSLocalizedDescriptionKey: message,
89+
])
90+
}
91+
return NSError(domain: "com.intii.CopilotForXcode", code: -1, userInfo: [
92+
NSLocalizedDescriptionKey: error.localizedDescription,
93+
])
2594
}
26-
return nil
2795
}

Core/Sources/Service/Workspace.swift

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ final class Workspace {
4646

4747
var filespaces = [URL: Filespace]()
4848
var isRealtimeSuggestionEnabled = false
49+
var realtimeSuggestionFulfillmentTasks = Set<Task<Void, Error>>()
4950

5051
private lazy var service: CopilotSuggestionServiceType = Environment
5152
.createSuggestionService(projectRootURL)
@@ -61,23 +62,104 @@ final class Workspace {
6162
) -> Bool {
6263
guard isRealtimeSuggestionEnabled else { return false }
6364
guard let filespace = filespaces[fileURL] else { return true }
64-
if let range = filespace.currentSuggestionLineRange,
65-
range.contains(cursorPosition.line)
66-
{ return false }
6765
if lines.hashValue != filespace.suggestionSourceSnapshot.linesHash { return true }
6866
if cursorPosition != filespace.suggestionSourceSnapshot.cursorPosition { return true }
6967
return false
7068
}
7169

72-
func getSuggestedCode(
70+
func getRealtimeSuggestedCode(
7371
forFileAt fileURL: URL,
7472
content: String,
7573
lines: [String],
7674
cursorPosition: CursorPosition,
7775
tabSize: Int,
7876
indentSize: Int,
7977
usesTabsForIndentation: Bool
80-
) async throws -> UpdatedContent {
78+
) -> UpdatedContent? {
79+
cancelAllRealtimeSuggestionFulfillmentTasks()
80+
guard isRealtimeSuggestionEnabled else { return nil }
81+
82+
let filespace = filespaces[fileURL] ?? .init(fileURL: fileURL)
83+
if filespaces[fileURL] == nil {
84+
filespaces[fileURL] = filespace
85+
}
86+
87+
let injector = SuggestionInjector()
88+
var extraInfo = SuggestionInjector.ExtraInfo()
89+
var lines = lines
90+
var cursorPosition = cursorPosition
91+
92+
injector.rejectCurrentSuggestions(
93+
from: &lines,
94+
cursorPosition: &cursorPosition,
95+
extraInfo: &extraInfo
96+
)
97+
98+
let snapshot = Filespace.Snapshot(linesHash: lines.hashValue, cursorPosition: cursorPosition)
99+
100+
if snapshot != filespace.suggestionSourceSnapshot {
101+
let task = Task {
102+
let result = try await getSuggestedCode(
103+
forFileAt: fileURL,
104+
content: content,
105+
lines: lines,
106+
cursorPosition: cursorPosition,
107+
tabSize: tabSize,
108+
indentSize: indentSize,
109+
usesTabsForIndentation: usesTabsForIndentation,
110+
shouldCancelAllRealtimeSuggestionFulfillmentTasks: false
111+
)
112+
try Task.checkCancellation()
113+
if result != nil {
114+
try? await Environment.triggerAction("Realtime Suggestions")
115+
}
116+
}
117+
118+
realtimeSuggestionFulfillmentTasks.insert(task)
119+
120+
return UpdatedContent(
121+
content: String(lines.joined(separator: "")),
122+
newCursor: cursorPosition,
123+
modifications: extraInfo.modifications
124+
)
125+
}
126+
127+
if filespace.suggestions.isEmpty || snapshot != filespace.suggestionSourceSnapshot {
128+
return .init(
129+
content: content,
130+
newCursor: cursorPosition,
131+
modifications: extraInfo.modifications
132+
)
133+
}
134+
135+
injector.proposeSuggestion(
136+
intoContentWithoutSuggestion: &lines,
137+
completion: filespace.suggestions[filespace.suggestionIndex],
138+
index: filespace.suggestionIndex,
139+
count: filespace.suggestions.count,
140+
extraInfo: &extraInfo
141+
)
142+
143+
return .init(
144+
content: String(lines.joined(separator: "")),
145+
newCursor: cursorPosition,
146+
modifications: extraInfo.modifications
147+
)
148+
}
149+
150+
func getSuggestedCode(
151+
forFileAt fileURL: URL,
152+
content: String,
153+
lines: [String],
154+
cursorPosition: CursorPosition,
155+
tabSize: Int,
156+
indentSize: Int,
157+
usesTabsForIndentation: Bool,
158+
shouldCancelAllRealtimeSuggestionFulfillmentTasks: Bool = true
159+
) async throws -> UpdatedContent? {
160+
if shouldCancelAllRealtimeSuggestionFulfillmentTasks {
161+
cancelAllRealtimeSuggestionFulfillmentTasks()
162+
}
81163
lastTriggerDate = Environment.now()
82164
let injector = SuggestionInjector()
83165
var lines = lines
@@ -88,17 +170,18 @@ final class Workspace {
88170
filespaces[fileURL] = filespace
89171
}
90172
var extraInfo = SuggestionInjector.ExtraInfo()
173+
let snapshot = Filespace.Snapshot(
174+
linesHash: lines.hashValue,
175+
cursorPosition: cursorPosition
176+
)
91177

92178
injector.rejectCurrentSuggestions(
93179
from: &lines,
94180
cursorPosition: &cursorPosition,
95181
extraInfo: &extraInfo
96182
)
97183

98-
filespace.suggestionSourceSnapshot = .init(
99-
linesHash: lines.hashValue,
100-
cursorPosition: cursorPosition
101-
)
184+
filespace.suggestionSourceSnapshot = snapshot
102185

103186
let completions = try await service.getCompletions(
104187
fileURL: fileURL,
@@ -108,6 +191,9 @@ final class Workspace {
108191
indentSize: indentSize,
109192
usesTabsForIndentation: usesTabsForIndentation
110193
)
194+
195+
guard filespace.suggestionSourceSnapshot == snapshot else { return nil }
196+
111197
if completions.isEmpty {
112198
return .init(
113199
content: content,
@@ -136,14 +222,15 @@ final class Workspace {
136222

137223
func getNextSuggestedCode(
138224
forFileAt fileURL: URL,
139-
content: String,
225+
content _: String,
140226
lines: [String],
141227
cursorPosition: CursorPosition
142-
) -> UpdatedContent {
228+
) -> UpdatedContent? {
229+
cancelAllRealtimeSuggestionFulfillmentTasks()
143230
lastTriggerDate = Environment.now()
144231
guard let filespace = filespaces[fileURL],
145232
filespace.suggestions.count > 1
146-
else { return .init(content: content, modifications: []) }
233+
else { return nil }
147234
var cursorPosition = cursorPosition
148235
filespace.suggestionIndex += 1
149236
if filespace.suggestionIndex >= filespace.suggestions.endIndex {
@@ -178,14 +265,15 @@ final class Workspace {
178265

179266
func getPreviousSuggestedCode(
180267
forFileAt fileURL: URL,
181-
content: String,
268+
content _: String,
182269
lines: [String],
183270
cursorPosition: CursorPosition
184-
) -> UpdatedContent {
271+
) -> UpdatedContent? {
272+
cancelAllRealtimeSuggestionFulfillmentTasks()
185273
lastTriggerDate = Environment.now()
186274
guard let filespace = filespaces[fileURL],
187275
filespace.suggestions.count > 1
188-
else { return .init(content: content, modifications: []) }
276+
else { return nil }
189277
var cursorPosition = cursorPosition
190278
filespace.suggestionIndex -= 1
191279
if filespace.suggestionIndex < 0 {
@@ -219,16 +307,17 @@ final class Workspace {
219307

220308
func getSuggestionAcceptedCode(
221309
forFileAt fileURL: URL,
222-
content: String,
310+
content _: String,
223311
lines: [String],
224312
cursorPosition: CursorPosition
225-
) -> UpdatedContent {
313+
) -> UpdatedContent? {
314+
cancelAllRealtimeSuggestionFulfillmentTasks()
226315
lastTriggerDate = Environment.now()
227316
guard let filespace = filespaces[fileURL],
228317
!filespace.suggestions.isEmpty,
229318
filespace.suggestionIndex >= 0,
230319
filespace.suggestionIndex < filespace.suggestions.endIndex
231-
else { return .init(content: content, modifications: []) }
320+
else { return nil }
232321

233322
var cursorPosition = cursorPosition
234323
var extraInfo = SuggestionInjector.ExtraInfo()
@@ -267,6 +356,7 @@ final class Workspace {
267356
lines: [String],
268357
cursorPosition: CursorPosition
269358
) -> UpdatedContent {
359+
cancelAllRealtimeSuggestionFulfillmentTasks()
270360
lastTriggerDate = Environment.now()
271361
let injector = SuggestionInjector()
272362
var lines = lines
@@ -299,4 +389,11 @@ extension Workspace {
299389
}
300390
}
301391
}
392+
393+
func cancelAllRealtimeSuggestionFulfillmentTasks() {
394+
for task in realtimeSuggestionFulfillmentTasks {
395+
task.cancel()
396+
}
397+
realtimeSuggestionFulfillmentTasks = []
398+
}
302399
}

0 commit comments

Comments
 (0)