Skip to content

Commit 832d7b7

Browse files
committed
Add a command dedicated to accept prompt to code suggestion
1 parent cbf25db commit 832d7b7

File tree

10 files changed

+212
-78
lines changed

10 files changed

+212
-78
lines changed

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */; };
1111
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */; };
1212
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; };
13+
C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */; };
1314
C81291D72994FE6900196E12 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C81291D52994FE6900196E12 /* Main.storyboard */; };
1415
C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C814588E2939EFDC00135263 /* Cocoa.framework */; };
1516
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458932939EFDC00135263 /* SourceEditorExtension.swift */; };
@@ -139,6 +140,7 @@
139140
C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRealtimeSuggestionsCommand.swift; sourceTree = "<group>"; };
140141
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealtimeSuggestionCommand.swift; sourceTree = "<group>"; };
141142
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSuggestionsCommand.swift; sourceTree = "<group>"; };
143+
C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptPromptToCodeCommand.swift; sourceTree = "<group>"; };
142144
C81291D52994FE6900196E12 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
143145
C81291D92994FE7900196E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
144146
C814588C2939EFDC00135263 /* Copilot.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Copilot.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -241,6 +243,7 @@
241243
C8520300293C4D9000460097 /* Helpers.swift */,
242244
C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */,
243245
C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */,
246+
C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */,
244247
C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */,
245248
C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */,
246249
C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */,
@@ -557,6 +560,7 @@
557560
C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */,
558561
C8520301293C4D9000460097 /* Helpers.swift in Sources */,
559562
C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */,
563+
C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */,
560564
C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */,
561565
C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */,
562566
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */,

Core/Sources/Client/AsyncXPCService.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ public struct AsyncXPCService {
8585
{ $0.getRealtimeSuggestedCode }
8686
)
8787
}
88+
89+
public func getPromptToCodeAcceptedCode(editorContent: EditorContent) async throws
90+
-> UpdatedContent?
91+
{
92+
try await suggestionRequest(
93+
connection,
94+
editorContent,
95+
{ $0.getPromptToCodeAcceptedCode }
96+
)
97+
}
8898

8999
public func toggleRealtimeSuggestion() async throws {
90100
try await withXPCServiceConnected(connection: connection) {

Core/Sources/Service/GUI/PromptToCodeProvider+Service.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ extension PromptToCodeProvider {
5858
onAcceptSuggestionTapped = { [weak self] in
5959
Task { [weak self] in
6060
let handler = PseudoCommandHandler()
61-
await handler.acceptSuggestion()
61+
await handler.acceptPromptToCode()
6262
if let app = ActiveApplicationMonitor.shared.previousApp,
6363
app.isXcode,
6464
!(self?.isContinuous ?? false)

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 99 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,45 @@ struct PseudoCommandHandler {
161161
}
162162
}
163163

164+
func acceptPromptToCode() async {
165+
do {
166+
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
167+
throw CancellationError()
168+
}
169+
try await Environment.triggerAction("Accept Prompt to Code")
170+
} catch {
171+
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
172+
?? ActiveApplicationMonitor.shared.latestXcode else { return }
173+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
174+
guard let focusElement = application.focusedElement,
175+
focusElement.description == "Source Editor"
176+
else { return }
177+
guard let (content, lines, _, cursorPosition) = await getFileContent(sourceEditor: nil)
178+
else {
179+
PresentInWindowSuggestionPresenter()
180+
.presentErrorMessage("Unable to get file content.")
181+
return
182+
}
183+
let handler = WindowBaseCommandHandler()
184+
do {
185+
guard let result = try await handler.acceptPromptToCode(editor: .init(
186+
content: content,
187+
lines: lines,
188+
uti: "",
189+
cursorPosition: cursorPosition,
190+
selections: [],
191+
tabSize: 0,
192+
indentSize: 0,
193+
usesTabsForIndentation: false
194+
)) else { return }
195+
196+
try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement)
197+
} catch {
198+
PresentInWindowSuggestionPresenter().presentError(error)
199+
}
200+
}
201+
}
202+
164203
func acceptSuggestion() async {
165204
do {
166205
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
@@ -174,12 +213,7 @@ struct PseudoCommandHandler {
174213
guard let focusElement = application.focusedElement,
175214
focusElement.description == "Source Editor"
176215
else { return }
177-
guard let (
178-
content,
179-
lines,
180-
_,
181-
cursorPosition
182-
) = await getFileContent(sourceEditor: nil)
216+
guard let (content, lines, _, cursorPosition) = await getFileContent(sourceEditor: nil)
183217
else {
184218
PresentInWindowSuggestionPresenter()
185219
.presentErrorMessage("Unable to get file content.")
@@ -198,52 +232,7 @@ struct PseudoCommandHandler {
198232
usesTabsForIndentation: false
199233
)) else { return }
200234

201-
let oldPosition = focusElement.selectedTextRange
202-
let oldScrollPosition = focusElement.parent?.verticalScrollBar?.doubleValue
203-
204-
let error = AXUIElementSetAttributeValue(
205-
focusElement,
206-
kAXValueAttribute as CFString,
207-
result.content as CFTypeRef
208-
)
209-
210-
if error != AXError.success {
211-
PresentInWindowSuggestionPresenter()
212-
.presentErrorMessage("Fail to set editor content.")
213-
}
214-
215-
if let selection = result.newSelection {
216-
var range = convertCursorRangeToRange(selection, in: result.content)
217-
if let value = AXValueCreate(.cfRange, &range) {
218-
AXUIElementSetAttributeValue(
219-
focusElement,
220-
kAXSelectedTextRangeAttribute as CFString,
221-
value
222-
)
223-
}
224-
} else if let oldPosition {
225-
var range = CFRange(
226-
location: oldPosition.lowerBound,
227-
length: 0
228-
)
229-
if let value = AXValueCreate(.cfRange, &range) {
230-
AXUIElementSetAttributeValue(
231-
focusElement,
232-
kAXSelectedTextRangeAttribute as CFString,
233-
value
234-
)
235-
}
236-
}
237-
238-
if let oldScrollPosition,
239-
let scrollBar = focusElement.parent?.verticalScrollBar
240-
{
241-
AXUIElementSetAttributeValue(
242-
scrollBar,
243-
kAXValueAttribute as CFString,
244-
oldScrollPosition as CFTypeRef
245-
)
246-
}
235+
try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement)
247236
} catch {
248237
PresentInWindowSuggestionPresenter().presentError(error)
249238
}
@@ -252,6 +241,64 @@ struct PseudoCommandHandler {
252241
}
253242

254243
extension PseudoCommandHandler {
244+
/// When Xcode commands are not available, we can fallback to directly
245+
/// set the value of the editor with Accessibility API.
246+
func injectUpdatedCodeWithAccessibilityAPI(
247+
_ result: UpdatedContent,
248+
focusElement: AXUIElement
249+
) throws {
250+
let oldPosition = focusElement.selectedTextRange
251+
let oldScrollPosition = focusElement.parent?.verticalScrollBar?.doubleValue
252+
253+
let error = AXUIElementSetAttributeValue(
254+
focusElement,
255+
kAXValueAttribute as CFString,
256+
result.content as CFTypeRef
257+
)
258+
259+
if error != AXError.success {
260+
PresentInWindowSuggestionPresenter()
261+
.presentErrorMessage("Fail to set editor content.")
262+
}
263+
264+
// recover selection range
265+
266+
if let selection = result.newSelection {
267+
var range = convertCursorRangeToRange(selection, in: result.content)
268+
if let value = AXValueCreate(.cfRange, &range) {
269+
AXUIElementSetAttributeValue(
270+
focusElement,
271+
kAXSelectedTextRangeAttribute as CFString,
272+
value
273+
)
274+
}
275+
} else if let oldPosition {
276+
var range = CFRange(
277+
location: oldPosition.lowerBound,
278+
length: 0
279+
)
280+
if let value = AXValueCreate(.cfRange, &range) {
281+
AXUIElementSetAttributeValue(
282+
focusElement,
283+
kAXSelectedTextRangeAttribute as CFString,
284+
value
285+
)
286+
}
287+
}
288+
289+
// recover scroll position
290+
291+
if let oldScrollPosition,
292+
let scrollBar = focusElement.parent?.verticalScrollBar
293+
{
294+
AXUIElementSetAttributeValue(
295+
scrollBar,
296+
kAXValueAttribute as CFString,
297+
oldScrollPosition as CFTypeRef
298+
)
299+
}
300+
}
301+
255302
func getFileContent(sourceEditor: AXUIElement?) async
256303
-> (
257304
content: String,

Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ protocol SuggestionCommandHandler {
1313
@ServiceActor
1414
func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent?
1515
@ServiceActor
16+
func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent?
17+
@ServiceActor
1618
func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
1719
@ServiceActor
1820
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,45 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
166166

167167
let dataSource = Service.shared.guiController.widgetDataSource
168168

169+
if let acceptedSuggestion = workspace.acceptSuggestion(
170+
forFileAt: fileURL,
171+
editor: editor
172+
) {
173+
injector.acceptSuggestion(
174+
intoContentWithoutSuggestion: &lines,
175+
cursorPosition: &cursorPosition,
176+
completion: acceptedSuggestion,
177+
extraInfo: &extraInfo
178+
)
179+
180+
presenter.discardSuggestion(fileURL: fileURL)
181+
182+
return .init(
183+
content: String(lines.joined(separator: "")),
184+
newSelection: .cursor(cursorPosition),
185+
modifications: extraInfo.modifications
186+
)
187+
}
188+
189+
return nil
190+
}
191+
192+
func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? {
193+
presenter.markAsProcessing(true)
194+
defer { presenter.markAsProcessing(false) }
195+
196+
let fileURL = try await Environment.fetchCurrentFileURL()
197+
198+
let injector = SuggestionInjector()
199+
var lines = editor.lines
200+
var cursorPosition = editor.cursorPosition
201+
var extraInfo = SuggestionInjector.ExtraInfo()
202+
203+
let dataSource = Service.shared.guiController.widgetDataSource
204+
169205
if let service = await dataSource.promptToCodes[fileURL]?.promptToCodeService {
170206
let rangeStart = service.selectionRange?.start ?? editor.cursorPosition
171-
207+
172208
let suggestion = CodeSuggestion(
173209
text: service.code,
174210
position: rangeStart,
@@ -203,24 +239,6 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
203239
newSelection: .init(start: rangeStart, end: cursorPosition),
204240
modifications: extraInfo.modifications
205241
)
206-
} else if let acceptedSuggestion = workspace.acceptSuggestion(
207-
forFileAt: fileURL,
208-
editor: editor
209-
) {
210-
injector.acceptSuggestion(
211-
intoContentWithoutSuggestion: &lines,
212-
cursorPosition: &cursorPosition,
213-
completion: acceptedSuggestion,
214-
extraInfo: &extraInfo
215-
)
216-
217-
presenter.discardSuggestion(fileURL: fileURL)
218-
219-
return .init(
220-
content: String(lines.joined(separator: "")),
221-
newSelection: .cursor(cursorPosition),
222-
modifications: extraInfo.modifications
223-
)
224242
}
225243

226244
return nil

Core/Sources/Service/XPCService.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ public class XPCService: NSObject, XPCServiceProtocol {
102102
try await handler.acceptSuggestion(editor: editor)
103103
}
104104
}
105+
106+
public func getPromptToCodeAcceptedCode(
107+
editorContent: Data,
108+
withReply reply: @escaping (Data?, Error?) -> Void
109+
) {
110+
replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in
111+
try await handler.acceptSuggestion(editor: editor)
112+
}
113+
}
105114

106115
public func getRealtimeSuggestedCode(
107116
editorContent: Data,

Core/Sources/XPCShared/XPCServiceProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public protocol XPCServiceProtocol {
2727
editorContent: Data,
2828
withReply reply: @escaping (Data?, Error?) -> Void
2929
)
30+
func getPromptToCodeAcceptedCode(
31+
editorContent: Data,
32+
withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void
33+
)
3034
func chatWithSelection(
3135
editorContent: Data,
3236
withReply reply: @escaping (Data?, Error?) -> Void
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Client
2+
import Foundation
3+
import SuggestionModel
4+
import XcodeKit
5+
6+
class AcceptPromptToCodeCommand: NSObject, XCSourceEditorCommand, CommandType {
7+
var name: String { "Accept Prompt to Code" }
8+
9+
func perform(
10+
with invocation: XCSourceEditorCommandInvocation,
11+
completionHandler: @escaping (Error?) -> Void
12+
) {
13+
Task {
14+
do {
15+
try await (Task(timeout: 7) {
16+
let service = try getService()
17+
if let content = try await service.getPromptToCodeAcceptedCode(
18+
editorContent: .init(invocation)
19+
) {
20+
invocation.accept(content)
21+
}
22+
completionHandler(nil)
23+
}.value)
24+
} catch is CancellationError {
25+
completionHandler(nil)
26+
} catch {
27+
completionHandler(error)
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)