Skip to content

Commit cd70355

Browse files
committed
Merge branch 'feature/real-time-suggestion-tweak' into develop
2 parents 18c6109 + d7d7e3e commit cd70355

15 files changed

Lines changed: 173 additions & 78 deletions

File tree

Core/Sources/CodeiumService/CodeiumService.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public protocol CodeiumSuggestionServiceType {
1818
func notifyOpenTextDocument(fileURL: URL, content: String) async throws
1919
func notifyChangeTextDocument(fileURL: URL, content: String) async throws
2020
func notifyCloseTextDocument(fileURL: URL) async throws
21+
func cancelRequest() async
2122
}
2223

2324
enum CodeiumError: Error, LocalizedError {
@@ -43,6 +44,7 @@ public class CodeiumSuggestionService {
4344
var server: CodeiumLSP?
4445
var heartbeatTask: Task<Void, Error>?
4546
var requestCounter: UInt64 = 0
47+
var cancellationCounter: UInt64 = 0
4648
let openedDocumentPool = OpenedDocumentPool()
4749
let onServiceLaunched: () -> Void
4850

@@ -118,6 +120,7 @@ public class CodeiumSuggestionService {
118120
self?.server = nil
119121
self?.heartbeatTask?.cancel()
120122
self?.requestCounter = 0
123+
self?.cancellationCounter = 0
121124
Logger.codeium.info("Language server is terminated, will be restarted when needed.")
122125
}
123126

@@ -227,7 +230,7 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType {
227230

228231
let relativePath = getRelativePath(of: fileURL)
229232

230-
let request = CodeiumRequest.GetCompletion(requestBody: .init(
233+
let request = await CodeiumRequest.GetCompletion(requestBody: .init(
231234
metadata: try getMetadata(),
232235
document: .init(
233236
absolute_path: fileURL.path,
@@ -253,8 +256,16 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType {
253256
)
254257
}
255258
))
259+
260+
if request.requestBody.metadata.request_id <= cancellationCounter {
261+
throw CancellationError()
262+
}
256263

257264
let result = try await (try await setupServerIfNeeded()).sendRequest(request)
265+
266+
if request.requestBody.metadata.request_id <= cancellationCounter {
267+
throw CancellationError()
268+
}
258269

259270
return result.completionItems?.filter { item in
260271
if ignoreSpaceOnlySuggestions {
@@ -280,6 +291,10 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType {
280291
)
281292
} ?? []
282293
}
294+
295+
public func cancelRequest() async {
296+
cancellationCounter = requestCounter
297+
}
283298

284299
public func notifyAccepted(_ suggestion: CodeSuggestion) async {
285300
_ = try? await (try setupServerIfNeeded())
@@ -291,7 +306,7 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType {
291306

292307
public func notifyOpenTextDocument(fileURL: URL, content: String) async throws {
293308
let relativePath = getRelativePath(of: fileURL)
294-
openedDocumentPool.openDocument(
309+
await openedDocumentPool.openDocument(
295310
url: fileURL,
296311
relativePath: relativePath,
297312
content: content
@@ -300,15 +315,15 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType {
300315

301316
public func notifyChangeTextDocument(fileURL: URL, content: String) async throws {
302317
let relativePath = getRelativePath(of: fileURL)
303-
openedDocumentPool.updateDocument(
318+
await openedDocumentPool.updateDocument(
304319
url: fileURL,
305320
relativePath: relativePath,
306321
content: content
307322
)
308323
}
309324

310325
public func notifyCloseTextDocument(fileURL: URL) async throws {
311-
openedDocumentPool.closeDocument(url: fileURL)
326+
await openedDocumentPool.closeDocument(url: fileURL)
312327
}
313328
}
314329

Core/Sources/CodeiumService/OpendDocumentPool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
private let maxSize: Int = 1_000_000 // Byte
44

5-
final class OpenedDocumentPool {
5+
actor OpenedDocumentPool {
66
var openedDocuments = [URL: OpenedDocument]()
77

88
func getOtherDocuments(exceptURL: URL) -> [OpenedDocument] {

Core/Sources/GitHubCopilotService/GitHubCopilotService.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public protocol GitHubCopilotSuggestionServiceType {
3131
func notifyChangeTextDocument(fileURL: URL, content: String) async throws
3232
func notifyCloseTextDocument(fileURL: URL) async throws
3333
func notifySaveTextDocument(fileURL: URL) async throws
34+
func cancelRequest() async
3435
}
3536

3637
protocol GitHubCopilotLSP {
@@ -311,6 +312,10 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
311312

312313
return try await task.value
313314
}
315+
316+
public func cancelRequest() async {
317+
await localProcessServer?.cancelOngoingTasks()
318+
}
314319

315320
public func notifyAccepted(_ completion: CodeSuggestion) async {
316321
_ = try? await server.sendRequest(

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99
import Logger
1010
import Preferences
1111
import QuartzCore
12+
import XcodeInspector
1213

1314
@ServiceActor
1415
public class RealtimeSuggestionController {
@@ -25,7 +26,7 @@ public class RealtimeSuggestionController {
2526
private var activeApplicationMonitorTask: Task<Void, Error>?
2627
private var editorObservationTask: Task<Void, Error>?
2728
private var focusedUIElement: AXUIElement?
28-
private var sourceEditor: AXUIElement?
29+
private var sourceEditor: SourceEditor?
2930

3031
var isCommentMode: Bool {
3132
UserDefaults.shared.value(for: \.suggestionPresentationMode) == .comment
@@ -48,9 +49,6 @@ public class RealtimeSuggestionController {
4849
await self.handleXcodeChanged(app)
4950
}
5051

51-
#warning(
52-
"TODO: Is it possible to get rid of hid event observation with only AXObserver?"
53-
)
5452
if ActiveApplicationMonitor.activeXcode != nil {
5553
await startHIDObservation(by: 1)
5654
} else {
@@ -118,7 +116,7 @@ public class RealtimeSuggestionController {
118116
}
119117

120118
guard focusElementType == "Source Editor" else { return }
121-
sourceEditor = focusElement
119+
sourceEditor = SourceEditor(runningApplication: activeXcode, element: focusElement)
122120

123121
editorObservationTask?.cancel()
124122
editorObservationTask = nil
@@ -127,7 +125,7 @@ public class RealtimeSuggestionController {
127125
let notificationsFromEditor = AXNotificationStream(
128126
app: activeXcode,
129127
element: focusElement,
130-
notificationNames: kAXValueChangedNotification
128+
notificationNames: kAXValueChangedNotification, kAXSelectedTextChangedNotification
131129
)
132130

133131
for await notification in notificationsFromEditor {
@@ -139,6 +137,10 @@ public class RealtimeSuggestionController {
139137
case kAXValueChangedNotification:
140138
self.triggerPrefetchDebounced()
141139
await self.notifyEditingFileChange(editor: focusElement)
140+
case kAXSelectedTextChangedNotification:
141+
guard let sourceEditor else { continue }
142+
await PseudoCommandHandler()
143+
.invalidateRealtimeSuggestionsIfNeeded(sourceEditor: sourceEditor)
142144
default:
143145
continue
144146
}
@@ -169,29 +171,16 @@ public class RealtimeSuggestionController {
169171
func handleHIDEvent(event: CGEvent) async {
170172
guard await Environment.isXcodeActive() else { return }
171173

172-
// Mouse clicks should cancel in-flight tasks.
173-
if [CGEventType.rightMouseDown, .leftMouseDown].contains(event.type) {
174-
await cancelInFlightTasks()
175-
return
176-
}
177-
178174
let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode))
179175
let escape = 0x35
180-
let arrowKeys = [0x7B, 0x7C, 0x7D, 0x7E]
181-
182-
// Arrow keys should cancel in-flight tasks.
183-
if arrowKeys.contains(keycode) {
184-
await cancelInFlightTasks()
185-
return
186-
}
187176

188177
// Escape should cancel in-flight tasks.
189178
// Except that when the completion panel is presented, it should trigger prefetch instead.
190179
if keycode == escape {
191180
if event.type == .keyDown {
192181
await cancelInFlightTasks()
193182
} else {
194-
let task = Task {
183+
Task {
195184
#warning(
196185
"TODO: Any method to avoid using AppleScript to check that completion panel is presented?"
197186
)
@@ -200,7 +189,6 @@ public class RealtimeSuggestionController {
200189
self.triggerPrefetchDebounced(force: true)
201190
}
202191
}
203-
inflightRealtimeSuggestionsTasks.insert(task)
204192
}
205193
}
206194
}
@@ -247,18 +235,6 @@ public class RealtimeSuggestionController {
247235
await workspace.cancelInFlightRealtimeSuggestionRequests()
248236
}
249237
}
250-
group.addTask {
251-
await { @ServiceActor in
252-
inflightRealtimeSuggestionsTasks.forEach {
253-
if $0 == excluding { return }
254-
$0.cancel()
255-
}
256-
inflightRealtimeSuggestionsTasks.removeAll()
257-
if let excluded = excluding {
258-
inflightRealtimeSuggestionsTasks.insert(excluded)
259-
}
260-
}()
261-
}
262238
}
263239
}
264240

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import ActiveApplicationMonitor
22
import AppKit
3-
import SuggestionModel
43
import Environment
54
import Preferences
65
import SuggestionInjector
6+
import SuggestionModel
7+
import XcodeInspector
78
import XPCShared
89

910
/// It's used to run some commands without really triggering the menu bar item.
@@ -38,9 +39,21 @@ struct PseudoCommandHandler {
3839
))
3940
}
4041

41-
func generateRealtimeSuggestions(sourceEditor: AXUIElement?) async {
42+
func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async {
4243
// Can't use handler if content is not available.
43-
guard let editor = await getEditorContent(sourceEditor: sourceEditor) else { return }
44+
guard
45+
let editor = await getEditorContent(sourceEditor: sourceEditor),
46+
let filespace = await getFilespace()
47+
else { return }
48+
49+
if await filespace.validateSuggestions(
50+
lines: editor.lines,
51+
cursorPosition: editor.cursorPosition
52+
) {
53+
return
54+
} else {
55+
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: filespace.fileURL)
56+
}
4457

4558
// Otherwise, get it from pseudo handler directly.
4659
let mode = UserDefaults.shared.value(for: \.suggestionPresentationMode)
@@ -54,6 +67,19 @@ struct PseudoCommandHandler {
5467
}
5568
}
5669

70+
func invalidateRealtimeSuggestionsIfNeeded(sourceEditor: SourceEditor) async {
71+
guard let fileURL = try? await Environment.fetchCurrentFileURL(),
72+
let (_, filespace) = try? await Workspace
73+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL) else { return }
74+
75+
if await !filespace.validateSuggestions(
76+
lines: sourceEditor.content.lines,
77+
cursorPosition: sourceEditor.content.cursorPosition
78+
) {
79+
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: fileURL)
80+
}
81+
}
82+
5783
func rejectSuggestions() async {
5884
let handler = WindowBaseCommandHandler()
5985
_ = try? await handler.rejectSuggestion(editor: .init(
@@ -214,7 +240,7 @@ extension PseudoCommandHandler {
214240
let content = focusElement.value
215241
let split = content.breakLines()
216242
let range = convertRangeToCursorRange(selectionRange, in: content)
217-
return (content, split, [range], range.end)
243+
return (content, split, [range], range.start)
218244
}
219245

220246
func getFileURL() async -> URL? {
@@ -232,11 +258,9 @@ extension PseudoCommandHandler {
232258
}
233259

234260
@ServiceActor
235-
func getEditorContent(sourceEditor: AXUIElement?) async -> EditorContent? {
236-
guard
237-
let filespace = await getFilespace(),
238-
let content = await getFileContent(sourceEditor: sourceEditor)
239-
else { return nil }
261+
func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? {
262+
guard let filespace = await getFilespace(), let sourceEditor else { return nil }
263+
let content = sourceEditor.content
240264
let uti = filespace.uti ?? ""
241265
let tabSize = filespace.tabSize ?? 4
242266
let indentSize = filespace.indentSize ?? 4
@@ -289,10 +313,10 @@ extension PseudoCommandHandler {
289313
var countE = 0
290314
var cursorRange = CursorRange(start: .zero, end: .outOfScope)
291315
for (i, line) in lines.enumerated() {
292-
if countS <= range.lowerBound && range.lowerBound < countS + line.count {
316+
if countS <= range.lowerBound, range.lowerBound < countS + line.count {
293317
cursorRange.start = .init(line: i, character: range.lowerBound - countS)
294318
}
295-
if countE <= range.upperBound && range.upperBound < countE + line.count {
319+
if countE <= range.upperBound, range.upperBound < countE + line.count {
296320
cursorRange.end = .init(line: i, character: range.upperBound - countE)
297321
break
298322
}
@@ -321,3 +345,4 @@ public extension String {
321345
return all
322346
}
323347
}
348+

0 commit comments

Comments
 (0)