Skip to content

Commit 6acaf19

Browse files
committed
Cherry pick accept first line of suggestion
1 parent 3b78254 commit 6acaf19

File tree

12 files changed

+337
-113
lines changed

12 files changed

+337
-113
lines changed

Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ struct SuggestionSettingsGeneralSectionView: View {
263263
var needControl
264264
@AppStorage(\.acceptSuggestionWithModifierOnlyForSwift)
265265
var onlyForSwift
266+
@AppStorage(\.acceptSuggestionLineWithModifierControl)
267+
var acceptLineWithControl
266268
}
267269

268270
@StateObject var settings = Settings()
@@ -290,6 +292,12 @@ struct SuggestionSettingsGeneralSectionView: View {
290292
Toggle(isOn: $settings.onlyForSwift) {
291293
Text("Only require modifiers for Swift")
292294
}
295+
296+
Divider()
297+
298+
Toggle(isOn: $settings.acceptLineWithControl) {
299+
Text("Accept suggestion first line with Control")
300+
}
293301
}
294302
.padding()
295303

Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift

Lines changed: 112 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -105,129 +105,139 @@ final class TabToAcceptSuggestion {
105105

106106
switch keycode {
107107
case tab:
108-
Logger.service.info("TabToAcceptSuggestion: Tab")
109-
110-
guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL
111-
else {
112-
Logger.service.info("TabToAcceptSuggestion: No active document")
113-
return .unchanged
114-
}
108+
return handleTab(event.flags)
109+
case esc:
110+
return handleEsc(event.flags)
111+
default:
112+
return .unchanged
113+
}
114+
}
115115

116-
let language = languageIdentifierFromFileURL(fileURL)
116+
func handleTab(_ flags: CGEventFlags) -> CGEventManipulation.Result {
117+
Logger.service.info("TabToAcceptSuggestion: Tab")
117118

118-
func checkKeybinding() -> Bool {
119-
if event.flags.contains(.maskHelp) { return false }
119+
guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL
120+
else {
121+
Logger.service.info("TabToAcceptSuggestion: No active document")
122+
return .unchanged
123+
}
120124

121-
let shouldCheckModifiers = if UserDefaults.shared
122-
.value(for: \.acceptSuggestionWithModifierOnlyForSwift)
123-
{
124-
language == .builtIn(.swift)
125-
} else {
126-
true
127-
}
125+
let language = languageIdentifierFromFileURL(fileURL)
128126

129-
if shouldCheckModifiers {
130-
if event.flags.contains(.maskShift) != UserDefaults.shared
131-
.value(for: \.acceptSuggestionWithModifierShift)
132-
{
133-
return false
134-
}
135-
if event.flags.contains(.maskControl) != UserDefaults.shared
136-
.value(for: \.acceptSuggestionWithModifierControl)
137-
{
138-
return false
139-
}
140-
if event.flags.contains(.maskAlternate) != UserDefaults.shared
141-
.value(for: \.acceptSuggestionWithModifierOption)
142-
{
143-
return false
144-
}
145-
if event.flags.contains(.maskCommand) != UserDefaults.shared
146-
.value(for: \.acceptSuggestionWithModifierCommand)
147-
{
148-
return false
149-
}
150-
} else {
151-
if event.flags.contains(.maskShift) { return false }
152-
if event.flags.contains(.maskControl) { return false }
153-
if event.flags.contains(.maskAlternate) { return false }
154-
if event.flags.contains(.maskCommand) { return false }
155-
}
127+
if flags.contains(.maskHelp) { return .unchanged }
156128

157-
return true
129+
let requiredFlagsToTrigger: CGEventFlags = {
130+
var all = CGEventFlags()
131+
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierShift) {
132+
all.insert(.maskShift)
158133
}
159-
160-
guard
161-
checkKeybinding(),
162-
canTapToAcceptSuggestion
163-
else {
164-
Logger.service.info("TabToAcceptSuggestion: Feature not available")
165-
return .unchanged
134+
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierControl) {
135+
all.insert(.maskControl)
166136
}
167-
168-
guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil
169-
else {
170-
Logger.service.info("TabToAcceptSuggestion: Xcode not found")
171-
return .unchanged
137+
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOption) {
138+
all.insert(.maskAlternate)
172139
}
173-
guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor
174-
else {
175-
Logger.service.info("TabToAcceptSuggestion: No editor found")
176-
return .unchanged
140+
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierCommand) {
141+
all.insert(.maskCommand)
177142
}
178-
guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL)
179-
else {
180-
Logger.service.info(
181-
"TabToAcceptSuggestion: No file found for file \(fileURL.lastPathComponent)"
182-
)
183-
return .unchanged
143+
if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOnlyForSwift) {
144+
if language == .builtIn(.swift) {
145+
return all
146+
} else {
147+
return []
148+
}
149+
} else {
150+
return all
184151
}
185-
guard let presentingSuggestion = filespace.presentingSuggestion
186-
else {
187-
Logger.service.info("TabToAcceptSuggestion: No Suggestions found")
152+
}()
153+
154+
let flagsToAvoidWhenNotRequired: [CGEventFlags] = [
155+
.maskShift, .maskCommand, .maskHelp, .maskSecondaryFn,
156+
]
157+
158+
guard flags.contains(requiredFlagsToTrigger) else {
159+
Logger.service.info("TabToAcceptSuggestion: Modifier not found")
160+
return .unchanged
161+
}
162+
163+
for flag in flagsToAvoidWhenNotRequired {
164+
if flags.contains(flag), !requiredFlagsToTrigger.contains(flag) {
188165
return .unchanged
189166
}
167+
}
190168

191-
let editorContent = editor.getContent()
169+
guard canTapToAcceptSuggestion else {
170+
Logger.service.info("TabToAcceptSuggestion: Feature not available")
171+
return .unchanged
172+
}
192173

193-
let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion(
194-
lines: editorContent.lines,
195-
cursorPosition: editorContent.cursorPosition,
196-
codeMetadata: filespace.codeMetadata,
197-
presentingSuggestionText: presentingSuggestion.text
198-
)
174+
guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil
175+
else {
176+
Logger.service.info("TabToAcceptSuggestion: Xcode not found")
177+
return .unchanged
178+
}
179+
guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor
180+
else {
181+
Logger.service.info("TabToAcceptSuggestion: No editor found")
182+
return .unchanged
183+
}
184+
guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL)
185+
else {
186+
Logger.service.info("TabToAcceptSuggestion: No file found")
187+
return .unchanged
188+
}
189+
guard let presentingSuggestion = filespace.presentingSuggestion
190+
else {
191+
Logger.service.info("TabToAcceptSuggestion: No Suggestions found")
192+
return .unchanged
193+
}
199194

200-
if shouldAcceptSuggestion {
201-
Logger.service.info("TabToAcceptSuggestion: Accept")
202-
Task { await commandHandler.acceptSuggestion() }
203-
return .discarded
195+
let editorContent = editor.getContent()
196+
197+
let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion(
198+
lines: editorContent.lines,
199+
cursorPosition: editorContent.cursorPosition,
200+
codeMetadata: filespace.codeMetadata,
201+
presentingSuggestionText: presentingSuggestion.text
202+
)
203+
204+
if shouldAcceptSuggestion {
205+
Logger.service.info("TabToAcceptSuggestion: Accept")
206+
if flags.contains(.maskControl),
207+
!requiredFlagsToTrigger.contains(.maskControl)
208+
{
209+
Task { await commandHandler.acceptActiveSuggestionLineInGroup(atIndex: nil)
210+
}
204211
} else {
205-
Logger.service.info("TabToAcceptSuggestion: Should not accept")
206-
return .unchanged
212+
Task { await commandHandler.acceptSuggestion() }
207213
}
208-
case esc:
209-
guard
210-
!event.flags.contains(.maskShift),
211-
!event.flags.contains(.maskControl),
212-
!event.flags.contains(.maskAlternate),
213-
!event.flags.contains(.maskCommand),
214-
!event.flags.contains(.maskHelp),
215-
canEscToDismissSuggestion
216-
else { return .unchanged }
217-
218-
guard
219-
let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL,
220-
ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil,
221-
let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL),
222-
filespace.presentingSuggestion != nil
223-
else { return .unchanged }
224-
225-
Task { await commandHandler.dismissSuggestion() }
226214
return .discarded
227-
default:
215+
} else {
216+
Logger.service.info("TabToAcceptSuggestion: Should not accept")
228217
return .unchanged
229218
}
230219
}
220+
221+
func handleEsc(_ flags: CGEventFlags) -> CGEventManipulation.Result {
222+
guard
223+
!flags.contains(.maskShift),
224+
!flags.contains(.maskControl),
225+
!flags.contains(.maskAlternate),
226+
!flags.contains(.maskCommand),
227+
!flags.contains(.maskHelp),
228+
canEscToDismissSuggestion
229+
else { return .unchanged }
230+
231+
guard
232+
let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL,
233+
ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil,
234+
let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL),
235+
filespace.presentingSuggestion != nil
236+
else { return .unchanged }
237+
238+
Task { await commandHandler.dismissSuggestion() }
239+
return .discarded
240+
}
231241
}
232242

233243
extension TabToAcceptSuggestion {

Core/Sources/Service/Service.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ public extension Service {
158158
}
159159
}
160160
}
161+
162+
try ExtensionServiceRequests.GetSuggestionLineAcceptedCode.handle(
163+
endpoint: endpoint,
164+
requestBody: requestBody,
165+
reply: reply
166+
) { request in
167+
let editor = request.editorContent
168+
let handler = WindowBaseCommandHandler()
169+
let updatedContent = try? await handler
170+
.acceptSuggestionLine(editor: editor)
171+
return updatedContent
172+
}
161173
} catch is XPCRequestHandlerHitError {
162174
return
163175
} catch {

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,68 @@ struct PseudoCommandHandler: CommandHandler {
282282
), sendImmediately: false)))
283283
}
284284

285+
func acceptActiveSuggestionLineInGroup(atIndex index: Int?) async {
286+
do {
287+
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
288+
throw CancellationError()
289+
}
290+
do {
291+
try await XcodeInspector.shared.safe.latestActiveXcode?
292+
.triggerCopilotCommand(name: "Accept Suggestion Line")
293+
} catch {
294+
let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI
295+
let now = Date()
296+
if now.timeIntervalSince(last) > 60 * 60 {
297+
Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now
298+
toast.toast(content: """
299+
The app is using a fallback solution to accept suggestions. \
300+
For better experience, please restart Xcode to re-activate the Copilot \
301+
menu item.
302+
""", type: .warning, duration: 10)
303+
}
304+
305+
throw error
306+
}
307+
} catch {
308+
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
309+
?? ActiveApplicationMonitor.shared.latestXcode else { return }
310+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
311+
guard let focusElement = application.focusedElement,
312+
focusElement.description == "Source Editor"
313+
else { return }
314+
guard let (
315+
content,
316+
lines,
317+
_,
318+
cursorPosition,
319+
cursorOffset
320+
) = await getFileContent(sourceEditor: nil)
321+
else {
322+
PresentInWindowSuggestionPresenter()
323+
.presentErrorMessage("Unable to get file content.")
324+
return
325+
}
326+
let handler = WindowBaseCommandHandler()
327+
do {
328+
guard let result = try await handler.acceptSuggestion(editor: .init(
329+
content: content,
330+
lines: lines,
331+
uti: "",
332+
cursorPosition: cursorPosition,
333+
cursorOffset: cursorOffset,
334+
selections: [],
335+
tabSize: 0,
336+
indentSize: 0,
337+
usesTabsForIndentation: false
338+
)) else { return }
339+
340+
try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement)
341+
} catch {
342+
PresentInWindowSuggestionPresenter().presentError(error)
343+
}
344+
}
345+
}
346+
285347
func acceptSuggestion() async {
286348
do {
287349
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
@@ -649,5 +711,34 @@ extension PseudoCommandHandler {
649711
usesTabsForIndentation: usesTabsForIndentation
650712
)
651713
}
714+
715+
func handleAcceptSuggestionLineCommand(editor: EditorContent) async throws -> CodeSuggestion? {
716+
guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL
717+
else { return nil }
718+
719+
return try await acceptSuggestionLineInGroup(
720+
atIndex: 0,
721+
editor: editor
722+
)
723+
}
724+
725+
func acceptSuggestionLineInGroup(
726+
atIndex index: Int?,
727+
editor: EditorContent
728+
) async throws -> CodeSuggestion? {
729+
guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL
730+
else { return nil }
731+
let (workspace, _) = try await Service.shared.workspacePool
732+
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
733+
734+
guard var acceptedSuggestion = await workspace.acceptSuggestion(
735+
forFileAt: fileURL,
736+
editor: editor
737+
) else { return nil }
738+
739+
let text = acceptedSuggestion.text
740+
acceptedSuggestion.text = String(text.splitByNewLine().first ?? "")
741+
return acceptedSuggestion
742+
}
652743
}
653744

0 commit comments

Comments
 (0)