Skip to content

Commit f8b34af

Browse files
committed
Merge branch 'release/0.30.2'
2 parents 077560c + d6dfc1f commit f8b34af

5 files changed

Lines changed: 111 additions & 33 deletions

File tree

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AppKit
33
import Preferences
44
import SuggestionInjector
55
import SuggestionModel
6+
import Toast
67
import Workspace
78
import WorkspaceSuggestionService
89
import XcodeInspector
@@ -12,6 +13,9 @@ import XPCShared
1213
///
1314
/// For example, we can use it to generate real-time suggestions without Apple Scripts.
1415
struct PseudoCommandHandler {
16+
static var lastTimeCommandFailedToTriggerWithAccessibilityAPI = Date(timeIntervalSince1970: 0)
17+
private var toast: ToastController { ToastControllerDependencyKey.liveValue }
18+
1519
func presentPreviousSuggestion() async {
1620
let handler = WindowBaseCommandHandler()
1721
_ = try? await handler.presentPreviousSuggestion(editor: .init(
@@ -43,11 +47,11 @@ struct PseudoCommandHandler {
4347
@WorkspaceActor
4448
func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async {
4549
guard let filespace = await getFilespace(),
46-
let (workspace, _) = try? await Service.shared.workspacePool
47-
.fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return }
48-
50+
let (workspace, _) = try? await Service.shared.workspacePool
51+
.fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return }
52+
4953
if Task.isCancelled { return }
50-
54+
5155
// Can't use handler if content is not available.
5256
guard let editor = await getEditorContent(sourceEditor: sourceEditor)
5357
else { return }
@@ -107,7 +111,7 @@ struct PseudoCommandHandler {
107111
if filespace.presentingSuggestion == nil {
108112
return // skip if there's no suggestion presented.
109113
}
110-
114+
111115
let content = sourceEditor.getContent()
112116
if !filespace.validateSuggestions(
113117
lines: content.lines,
@@ -178,8 +182,23 @@ struct PseudoCommandHandler {
178182
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
179183
throw CancellationError()
180184
}
181-
try await XcodeInspector.shared.latestActiveXcode?
182-
.triggerCopilotCommand(name: "Accept Prompt to Code")
185+
do {
186+
try await XcodeInspector.shared.latestActiveXcode?
187+
.triggerCopilotCommand(name: "Accept Prompt to Code")
188+
} catch {
189+
let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI
190+
let now = Date()
191+
if now.timeIntervalSince(last) > 60 * 60 {
192+
Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now
193+
toast.toast(content: """
194+
The app is using a fallback solution to accept suggestions. \
195+
For better experience, please restart Xcode to re-activate the Copilot \
196+
menu item.
197+
""", type: .warning)
198+
}
199+
200+
throw error
201+
}
183202
} catch {
184203
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
185204
?? ActiveApplicationMonitor.shared.latestXcode else { return }
@@ -218,8 +237,23 @@ struct PseudoCommandHandler {
218237
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
219238
throw CancellationError()
220239
}
221-
try await XcodeInspector.shared.latestActiveXcode?
222-
.triggerCopilotCommand(name: "Accept Suggestion")
240+
do {
241+
try await XcodeInspector.shared.latestActiveXcode?
242+
.triggerCopilotCommand(name: "Accept Suggestion")
243+
} catch {
244+
let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI
245+
let now = Date()
246+
if now.timeIntervalSince(last) > 60 * 60 {
247+
Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now
248+
toast.toast(content: """
249+
The app is using a fallback solution to accept suggestions. \
250+
For better experience, please restart Xcode to re-activate the Copilot \
251+
menu item.
252+
""", type: .warning)
253+
}
254+
255+
throw error
256+
}
223257
} catch {
224258
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
225259
?? ActiveApplicationMonitor.shared.latestXcode else { return }
@@ -252,12 +286,12 @@ struct PseudoCommandHandler {
252286
}
253287
}
254288
}
255-
289+
256290
func dismissSuggestion() async {
257291
guard let documentURL = XcodeInspector.shared.activeDocumentURL else { return }
258292
guard let (_, filespace) = try? await Service.shared.workspacePool
259293
.fetchOrCreateWorkspaceAndFilespace(fileURL: documentURL) else { return }
260-
294+
261295
await filespace.reset()
262296
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: documentURL)
263297
}
@@ -432,3 +466,4 @@ extension PseudoCommandHandler {
432466
return cursorRange
433467
}
434468
}
469+

Pro

Submodule Pro updated from 53c37b4 to bdca40b

Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,56 @@ public extension XcodeAppInstanceInspector {
77
func triggerCopilotCommand(name: String, activateXcode: Bool = true) async throws {
88
let bundleName = Bundle.main
99
.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
10-
try await triggerMenuItem(path: ["Editor", bundleName, name], activateXcode: activateXcode)
10+
try await triggerMenuItem(path: ["Editor", bundleName, name], activateApp: activateXcode)
1111
}
1212
}
1313

1414
public extension AppInstanceInspector {
15-
@MainActor
16-
func triggerMenuItem(path: [String], activateXcode: Bool) async throws {
17-
guard !path.isEmpty else { return }
15+
struct CantRunCommand: Error, LocalizedError {
16+
let path: String
17+
let reason: String
18+
public var errorDescription: String? {
19+
"Can't run command \(path): \(reason)"
20+
}
21+
}
1822

19-
struct CantRunCommand: Error, LocalizedError {
20-
let path: [String]
21-
var errorDescription: String? {
22-
"Can't run command \(path.joined(separator: "/"))."
23-
}
23+
@MainActor
24+
func triggerMenuItem(path: [String], activateApp: Bool) async throws {
25+
let sourcePath = path.joined(separator: "/")
26+
func cantRunCommand(_ reason: String) -> CantRunCommand {
27+
return CantRunCommand(path: sourcePath, reason: reason)
2428
}
2529

26-
if activateXcode {
30+
guard path.count >= 2 else { throw cantRunCommand("Path too short.") }
31+
32+
if activateApp {
2733
if !runningApplication.activate() {
28-
throw CantRunCommand(path: path)
34+
Logger.service.error("""
35+
Trigger menu item \(sourcePath) failed: \
36+
Xcode not activated.
37+
""")
2938
}
3039
} else {
3140
if !runningApplication.isActive {
32-
throw CantRunCommand(path: path)
41+
Logger.service.error("""
42+
Trigger menu item \(sourcePath) failed: \
43+
Xcode not activated.
44+
""")
3345
}
3446
}
3547

3648
await Task.yield()
3749

3850
if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) {
3951
let app = AXUIElementCreateApplication(runningApplication.processIdentifier)
40-
guard let menuBar = app.menuBar else { throw CantRunCommand(path: path) }
52+
53+
guard let menuBar = app.menuBar else {
54+
Logger.service.error("""
55+
Trigger menu item \(sourcePath) failed: \
56+
Menu not found.
57+
""")
58+
throw cantRunCommand("Menu not found.")
59+
}
4160
var path = path
4261
var currentMenu = menuBar
4362
while !path.isEmpty {
@@ -47,22 +66,34 @@ public extension AppInstanceInspector {
4766
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
4867
if error != AXError.success {
4968
Logger.service.error("""
50-
Trigger menu item \(path.joined(separator: "/")) failed: \
69+
Trigger menu item \(sourcePath) failed: \
5170
\(error.localizedDescription)
5271
""")
53-
throw error
72+
throw cantRunCommand(error.localizedDescription)
5473
} else {
74+
#if DEBUG
75+
Logger.service.info("""
76+
Trigger menu item \(sourcePath) succeeded.
77+
""")
78+
#endif
5579
return
5680
}
5781
} else if let menu = currentMenu.child(title: item) {
82+
#if DEBUG
83+
Logger.service.info("""
84+
Trigger menu item \(sourcePath): Move to \(item).
85+
""")
86+
#endif
5887
currentMenu = menu
5988
} else {
60-
throw CantRunCommand(path: path)
89+
Logger.service.error("""
90+
Trigger menu item \(sourcePath) failed: \
91+
\(item) is not found.
92+
""")
93+
throw cantRunCommand("\(item) is not found.")
6194
}
6295
}
6396
} else {
64-
guard path.count >= 2 else { throw CantRunCommand(path: path) }
65-
6697
let clickTask = {
6798
var path = path
6899
let button = path.removeLast()
@@ -103,7 +134,7 @@ public extension AppInstanceInspector {
103134
Trigger menu item \(path.joined(separator: "/")) failed: \
104135
\(error.localizedDescription)
105136
""")
106-
throw error
137+
throw cantRunCommand(error.localizedDescription)
107138
}
108139
}
109140
}

Version.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
APP_VERSION = 0.30.1
2-
APP_BUILD = 313
1+
APP_VERSION = 0.30.2
2+
APP_BUILD = 314
33

appcast.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
<channel>
44
<title>Copilot for Xcode</title>
55

6+
<item>
7+
<title>0.30.2</title>
8+
<pubDate>Thu, 01 Feb 2024 15:09:51 +0800</pubDate>
9+
<sparkle:version>314</sparkle:version>
10+
<sparkle:shortVersionString>0.30.2</sparkle:shortVersionString>
11+
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
12+
<sparkle:releaseNotesLink>
13+
https://github.com/intitni/CopilotForXcode/releases/tag/0.30.2
14+
</sparkle:releaseNotesLink>
15+
<enclosure url="https://github.com/intitni/CopilotForXcode/releases/download/0.30.2/Copilot.for.Xcode.app.zip" length="41214394" type="application/octet-stream" sparkle:edSignature="6a/v6PRi2JuGnlaRTNueUlO5v7T196/FcHavwSRyT8lkj8WYGm3PhFJqGJz87oyIgVZfhGsohYigBVGNDn8DCA=="/>
16+
</item>
17+
618
<item>
719
<title>0.30.1</title>
820
<pubDate>Sun, 28 Jan 2024 17:12:35 +0800</pubDate>

0 commit comments

Comments
 (0)