Skip to content

Commit 9321b5c

Browse files
committed
Merge branch 'hotfix/0.19.1'
2 parents ccdf016 + 416399a commit 9321b5c

File tree

10 files changed

+254
-84
lines changed

10 files changed

+254
-84
lines changed

Copilot for Xcode.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; };
3939
C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4040
C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; };
41+
C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */; };
4142
C8A3AE592A2885A70046E809 /* InitializePython.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8A3AE582A2885A70046E809 /* InitializePython.swift */; };
4243
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4344
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; };
@@ -166,6 +167,7 @@
166167
C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextSuggestionCommand.swift; sourceTree = "<group>"; };
167168
C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviousSuggestionCommand.swift; sourceTree = "<group>"; };
168169
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = "<group>"; };
170+
C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = "<group>"; };
169171
C8A3AE512A2883430046E809 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = "<group>"; };
170172
C8A3AE582A2885A70046E809 /* InitializePython.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitializePython.swift; sourceTree = "<group>"; };
171173
C8A3AE5A2A288AF90046E809 /* site-packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "site-packages"; sourceTree = "<group>"; };
@@ -322,6 +324,7 @@
322324
C81291D92994FE7900196E12 /* Info.plist */,
323325
C861E61F2994F6390056CB02 /* ServiceDelegate.swift */,
324326
C861E6102994F6070056CB02 /* AppDelegate.swift */,
327+
C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */,
325328
C8A3AE582A2885A70046E809 /* InitializePython.swift */,
326329
C81291D52994FE6900196E12 /* Main.storyboard */,
327330
C861E6142994F6080056CB02 /* Assets.xcassets */,
@@ -583,6 +586,7 @@
583586
buildActionMask = 2147483647;
584587
files = (
585588
C8A3AE592A2885A70046E809 /* InitializePython.swift in Sources */,
589+
C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */,
586590
C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */,
587591
C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */,
588592
);

Core/Sources/AXNotificationStream/AXNotificationStream.swift

+47-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AppKit
22
import ApplicationServices
33
import Foundation
4+
import Logger
45

56
public final class AXNotificationStream: AsyncSequence {
67
public typealias Stream = AsyncStream<Element>
@@ -18,7 +19,7 @@ public final class AXNotificationStream: AsyncSequence {
1819
deinit {
1920
continuation.finish()
2021
}
21-
22+
2223
public convenience init(
2324
app: NSRunningApplication,
2425
element: AXUIElement? = nil,
@@ -72,24 +73,56 @@ public final class AXNotificationStream: AsyncSequence {
7273
.commonModes
7374
)
7475
}
75-
76-
Task {
77-
for name in notificationNames {
78-
var error = AXError.cannotComplete
79-
var retryCount = 0
80-
while error == AXError.cannotComplete, retryCount < 5 {
81-
error = AXObserverAddNotification(observer, observingElement, name as CFString, &continuation)
82-
if error == .cannotComplete {
83-
try await Task.sleep(nanoseconds: 1_000_000_000)
84-
}
85-
retryCount += 1
86-
}
87-
}
76+
77+
Task { [weak self] in
8878
CFRunLoopAddSource(
8979
CFRunLoopGetMain(),
9080
AXObserverGetRunLoopSource(observer),
9181
.commonModes
9282
)
83+
var pendingRegistrationNames = Set(notificationNames)
84+
var retry = 0
85+
while !pendingRegistrationNames.isEmpty, retry < 100 {
86+
guard let self else { return }
87+
retry += 1
88+
for name in notificationNames {
89+
let e = AXObserverAddNotification(
90+
observer,
91+
observingElement,
92+
name as CFString,
93+
&self.continuation
94+
)
95+
switch e {
96+
case .success:
97+
pendingRegistrationNames.remove(name)
98+
case .actionUnsupported:
99+
Logger.service.error("AXObserver: Action unsupported: \(name)")
100+
pendingRegistrationNames.remove(name)
101+
case .apiDisabled:
102+
Logger.service.error("AXObserver: Accessibility API disabled, will try again later")
103+
retry -= 1
104+
case .invalidUIElement:
105+
Logger.service.error("AXObserver: Invalid UI element")
106+
pendingRegistrationNames.remove(name)
107+
case .invalidUIElementObserver:
108+
Logger.service.error("AXObserver: Invalid UI element observer")
109+
pendingRegistrationNames.remove(name)
110+
case .cannotComplete:
111+
Logger.service
112+
.error("AXObserver: Failed to observe \(name), will try again later")
113+
case .notificationUnsupported:
114+
Logger.service.error("AXObserver: Notification unsupported: \(name)")
115+
pendingRegistrationNames.remove(name)
116+
case .notificationAlreadyRegistered:
117+
pendingRegistrationNames.remove(name)
118+
default:
119+
Logger.service
120+
.error("AXObserver: Unrecognized error \(e) when registering \(name), will try again later")
121+
}
122+
}
123+
try await Task.sleep(nanoseconds: 1_500_000_000)
124+
}
93125
}
94126
}
95127
}
128+

Core/Sources/HostApp/DebugView.swift

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ final class DebugSettings: ObservableObject {
1010
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
1111
@AppStorage(\.alwaysAcceptSuggestionWithAccessibilityAPI)
1212
var alwaysAcceptSuggestionWithAccessibilityAPI
13+
@AppStorage(\.enableXcodeInspectorDebugMenu) var enableXcodeInspectorDebugMenu
1314
init() {}
1415
}
1516

@@ -40,6 +41,9 @@ struct DebugSettingsView: View {
4041
Toggle(isOn: $settings.alwaysAcceptSuggestionWithAccessibilityAPI) {
4142
Text("Always accept suggestion with AccessibilityAPI")
4243
}
44+
Toggle(isOn: $settings.enableXcodeInspectorDebugMenu) {
45+
Text("Enable Xcode inspector debug menu")
46+
}
4347
}
4448
.padding()
4549
}

Core/Sources/Service/ScheduledCleaner.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public final class ScheduledCleaner {
3434
XcodeAppInstanceInspector.WorkspaceInfo
3535
]()
3636
) { result, xcode in
37-
let infos = xcode.workspaces
37+
let infos = xcode.realtimeWorkspaces
3838
for (id, info) in infos {
3939
if let existed = result[id] {
4040
result[id] = existed.combined(with: info)

Core/Sources/XcodeInspector/XcodeInspector.swift

+25-10
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class XcodeInspector: ObservableObject {
3737
if let activeXcode {
3838
setActiveXcode(activeXcode)
3939
}
40-
40+
4141
let sequence = NSWorkspace.shared.notificationCenter
4242
.notifications(named: NSWorkspace.didActivateApplicationNotification)
4343
for await notification in sequence {
@@ -90,7 +90,7 @@ public final class XcodeInspector: ObservableObject {
9090
@MainActor
9191
func setActiveXcode(_ xcode: XcodeAppInstanceInspector) {
9292
xcode.refresh()
93-
93+
9494
for task in activeXcodeObservations { task.cancel() }
9595
for cancellable in activeXcodeCancellable { cancellable.cancel() }
9696
activeXcodeObservations.removeAll()
@@ -178,6 +178,10 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
178178
@Published public var documentURL: URL = .init(fileURLWithPath: "/")
179179
@Published public var projectURL: URL = .init(fileURLWithPath: "/")
180180
@Published public var workspaces = [WorkspaceIdentifier: WorkspaceInfo]()
181+
public var realtimeWorkspaces: [WorkspaceIdentifier: WorkspaceInfo] {
182+
Self.fetchWorkspaceInfo(runningApplication)
183+
}
184+
181185
@Published public private(set) var completionPanel: AXUIElement?
182186

183187
var _version: String?
@@ -211,7 +215,15 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
211215
super.init(runningApplication: runningApplication)
212216

213217
observeFocusedWindow()
214-
observe()
218+
observeAXNotifications()
219+
220+
Task {
221+
try await Task.sleep(nanoseconds: 3_000_000_000)
222+
// Sometimes the focused window may not be ready on app launch.
223+
if !(focusedWindow is WorkspaceXcodeWindowInspector) {
224+
observeFocusedWindow()
225+
}
226+
}
215227
}
216228

217229
func observeFocusedWindow() {
@@ -246,16 +258,19 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
246258
focusedWindow = nil
247259
}
248260
}
249-
261+
250262
func refresh() {
251-
(focusedWindow as? WorkspaceXcodeWindowInspector)?.refresh()
252-
observe()
263+
if let focusedWindow = focusedWindow as? WorkspaceXcodeWindowInspector {
264+
focusedWindow.refresh()
265+
} else {
266+
observeFocusedWindow()
267+
}
253268
}
254-
255-
func observe() {
269+
270+
func observeAXNotifications() {
256271
longRunningTasks.forEach { $0.cancel() }
257272
longRunningTasks = []
258-
273+
259274
let focusedWindowChanged = Task {
260275
let notification = AXNotificationStream(
261276
app: runningApplication,
@@ -277,7 +292,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
277292
kAXApplicationDeactivatedNotification
278293
)
279294
if #available(macOS 13.0, *) {
280-
for await _ in notification.debounce(for: .seconds(5)) {
295+
for await _ in notification.debounce(for: .seconds(2)) {
281296
try Task.checkCancellation()
282297
workspaces = Self.fetchWorkspaceInfo(runningApplication)
283298
}
+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import AppKit
2+
import Foundation
3+
import Preferences
4+
import XcodeInspector
5+
6+
extension AppDelegate {
7+
fileprivate var statusBarMenuIdentifier: NSUserInterfaceItemIdentifier {
8+
.init("statusBarMenu")
9+
}
10+
11+
fileprivate var xcodeInspectorDebugMenuIdentifier: NSUserInterfaceItemIdentifier {
12+
.init("xcodeInspectorDebugMenu")
13+
}
14+
15+
@objc func buildStatusBarMenu() {
16+
let statusBar = NSStatusBar.system
17+
statusBarItem = statusBar.statusItem(
18+
withLength: NSStatusItem.squareLength
19+
)
20+
statusBarItem.button?.image = NSImage(named: "MenuBarIcon")
21+
22+
let statusBarMenu = NSMenu(title: "Status Bar Menu")
23+
statusBarMenu.identifier = statusBarMenuIdentifier
24+
statusBarItem.menu = statusBarMenu
25+
26+
let hostAppName = Bundle.main.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String
27+
?? "Copilot for Xcode"
28+
29+
let copilotName = NSMenuItem(
30+
title: hostAppName,
31+
action: nil,
32+
keyEquivalent: ""
33+
)
34+
35+
let checkForUpdate = NSMenuItem(
36+
title: "Check for Updates",
37+
action: #selector(checkForUpdate),
38+
keyEquivalent: ""
39+
)
40+
41+
let openCopilotForXcode = NSMenuItem(
42+
title: "Open \(hostAppName)",
43+
action: #selector(openCopilotForXcode),
44+
keyEquivalent: ""
45+
)
46+
47+
let openGlobalChat = NSMenuItem(
48+
title: "Open Chat",
49+
action: #selector(openGlobalChat),
50+
keyEquivalent: ""
51+
)
52+
53+
let xcodeInspectorDebug = NSMenuItem(
54+
title: "Xcode Inspector Debug",
55+
action: nil,
56+
keyEquivalent: ""
57+
)
58+
59+
let xcodeInspectorDebugMenu = NSMenu(title: "Xcode Inspector Debug")
60+
xcodeInspectorDebugMenu.identifier = xcodeInspectorDebugMenuIdentifier
61+
xcodeInspectorDebug.submenu = xcodeInspectorDebugMenu
62+
xcodeInspectorDebug.isHidden = false
63+
64+
let quitItem = NSMenuItem(
65+
title: "Quit",
66+
action: #selector(quit),
67+
keyEquivalent: ""
68+
)
69+
quitItem.target = self
70+
71+
statusBarMenu.addItem(copilotName)
72+
statusBarMenu.addItem(openCopilotForXcode)
73+
statusBarMenu.addItem(checkForUpdate)
74+
statusBarMenu.addItem(.separator())
75+
statusBarMenu.addItem(openGlobalChat)
76+
statusBarMenu.addItem(.separator())
77+
statusBarMenu.addItem(xcodeInspectorDebug)
78+
statusBarMenu.addItem(quitItem)
79+
80+
statusBarMenu.delegate = self
81+
xcodeInspectorDebugMenu.delegate = self
82+
}
83+
}
84+
85+
extension AppDelegate: NSMenuDelegate {
86+
func menuWillOpen(_ menu: NSMenu) {
87+
switch menu.identifier {
88+
case statusBarMenuIdentifier:
89+
if let xcodeInspectorDebug = menu.items.first(where: { item in
90+
item.submenu?.identifier == xcodeInspectorDebugMenuIdentifier
91+
}) {
92+
xcodeInspectorDebug.isHidden = !UserDefaults.shared
93+
.value(for: \.enableXcodeInspectorDebugMenu)
94+
}
95+
case xcodeInspectorDebugMenuIdentifier:
96+
let inspector = XcodeInspector.shared
97+
menu.items.removeAll()
98+
menu.items.append(.text("Active Project: \(inspector.activeProjectURL)"))
99+
menu.items.append(.text("Active Document: \(inspector.activeDocumentURL)"))
100+
for xcode in inspector.xcodes {
101+
let item = NSMenuItem(
102+
title: "Xcode \(xcode.runningApplication.processIdentifier)",
103+
action: nil,
104+
keyEquivalent: ""
105+
)
106+
menu.addItem(item)
107+
let xcodeMenu = NSMenu()
108+
item.submenu = xcodeMenu
109+
xcodeMenu.items.append(.text("Is Active: \(xcode.isActive)"))
110+
xcodeMenu.items.append(.text("Active Project: \(xcode.projectURL)"))
111+
xcodeMenu.items.append(.text("Active Document: \(xcode.documentURL)"))
112+
113+
for (key, workspace) in xcode.realtimeWorkspaces {
114+
let workspaceItem = NSMenuItem(
115+
title: "Workspace \(key)",
116+
action: nil,
117+
keyEquivalent: ""
118+
)
119+
xcodeMenu.items.append(workspaceItem)
120+
let workspaceMenu = NSMenu()
121+
workspaceItem.submenu = workspaceMenu
122+
let tabsItem = NSMenuItem(
123+
title: "Tabs",
124+
action: nil,
125+
keyEquivalent: ""
126+
)
127+
workspaceMenu.addItem(tabsItem)
128+
let tabsMenu = NSMenu()
129+
tabsItem.submenu = tabsMenu
130+
for tab in workspace.tabs {
131+
tabsMenu.addItem(.text(tab))
132+
}
133+
}
134+
}
135+
default:
136+
break
137+
}
138+
}
139+
}
140+
141+
private extension NSMenuItem {
142+
static func text(_ text: String) -> NSMenuItem {
143+
let item = NSMenuItem(
144+
title: text,
145+
action: nil,
146+
keyEquivalent: ""
147+
)
148+
item.isEnabled = false
149+
return item
150+
}
151+
}

0 commit comments

Comments
 (0)