Skip to content

Commit 6943df0

Browse files
committed
Merge branch 'release/0.14.0'
2 parents ab95046 + 5748cb2 commit 6943df0

29 files changed

Lines changed: 910 additions & 390 deletions

Copilot for Xcode/DebugView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ final class DebugSettings: ObservableObject {
88
@AppStorage(\.preCacheOnFileOpen)
99
var preCacheOnFileOpen: Bool
1010
@AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround
11+
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
1112
init() {}
1213
}
1314

@@ -29,6 +30,10 @@ struct DebugSettingsView: View {
2930
Text("Use custom scroll view workaround for smooth scrolling")
3031
}
3132
.toggleStyle(.switch)
33+
Toggle(isOn: $settings.triggerActionWithAccessibilityAPI) {
34+
Text("Trigger action with AccessibilityAPI")
35+
}
36+
.toggleStyle(.switch)
3237
}
3338
}
3439
.buttonStyle(.copilot)

Copilot for Xcode/SettingsView.swift

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct SettingsView: View {
2828
var preferWidgetToStayInsideEditorWhenWidthGreaterThan: Double
2929
@AppStorage(\.hideCommonPrecedingSpacesInSuggestion)
3030
var hideCommonPrecedingSpacesInSuggestion: Bool
31+
@AppStorage(\.suggestionCodeFontSize) var suggestionCodeFontSize
32+
@AppStorage(\.chatFontSize) var chatFontSize
33+
@AppStorage(\.chatCodeFontSize) var chatCodeFontSize
3134
init() {}
3235
}
3336

@@ -49,7 +52,7 @@ struct SettingsView: View {
4952
isOpen: $isCustomCommandEditorOpen
5053
)
5154
}
52-
55+
5356
Form {
5457
Toggle(isOn: $settings.quitXPCServiceOnXcodeAndAppQuit) {
5558
Text("Quit service when Xcode and host app are terminated")
@@ -165,7 +168,10 @@ struct SettingsView: View {
165168
case .openAI:
166169
Text("OpenAI").tag($0)
167170
case .githubCopilot:
168-
Text("GitHub Copilot (Implement for experiment, barely works, don't use.)").tag($0)
171+
Text(
172+
"GitHub Copilot (Implement for experiment, barely works, don't use.)"
173+
)
174+
.tag($0)
169175
}
170176
}
171177
} label: {
@@ -184,7 +190,48 @@ struct SettingsView: View {
184190
}
185191
.textFieldStyle(.roundedBorder)
186192

187-
Text("px")
193+
Text("pt")
194+
}
195+
196+
Group { // UI
197+
HStack {
198+
TextField(text: .init(get: {
199+
"\(Int(settings.chatFontSize))"
200+
}, set: {
201+
settings.chatFontSize = Double(Int($0) ?? 0)
202+
})) {
203+
Text("Font size of chat message")
204+
}
205+
.textFieldStyle(.roundedBorder)
206+
207+
Text("pt")
208+
}
209+
210+
HStack {
211+
TextField(text: .init(get: {
212+
"\(Int(settings.chatCodeFontSize))"
213+
}, set: {
214+
settings.chatCodeFontSize = Double(Int($0) ?? 0)
215+
})) {
216+
Text("Font size of code block in chat")
217+
}
218+
.textFieldStyle(.roundedBorder)
219+
220+
Text("pt")
221+
}
222+
223+
HStack {
224+
TextField(text: .init(get: {
225+
"\(Int(settings.suggestionCodeFontSize))"
226+
}, set: {
227+
settings.suggestionCodeFontSize = Double(Int($0) ?? 0)
228+
})) {
229+
Text("Font size of suggestion code")
230+
}
231+
.textFieldStyle(.roundedBorder)
232+
233+
Text("pt")
234+
}
188235
}
189236
}
190237
}.buttonStyle(.copilot)

Core/Sources/AXExtension/AXUIElement.swift

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ public extension AXUIElement {
1212
(try? copyValue(key: kAXValueAttribute)) ?? ""
1313
}
1414

15+
var title: String {
16+
(try? copyValue(key: kAXTitleAttribute)) ?? ""
17+
}
18+
19+
var role: String {
20+
(try? copyValue(key: kAXRoleAttribute)) ?? ""
21+
}
22+
1523
var doubleValue: Double {
1624
(try? copyValue(key: kAXValueAttribute)) ?? 0.0
1725
}
@@ -23,6 +31,10 @@ public extension AXUIElement {
2331
var description: String {
2432
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
2533
}
34+
35+
var label: String {
36+
(try? copyValue(key: kAXLabelValueAttribute)) ?? ""
37+
}
2638

2739
var isSourceEditor: Bool {
2840
description == "Source Editor"
@@ -115,14 +127,34 @@ public extension AXUIElement {
115127
(try? copyValue(key: kAXChildrenAttribute)) ?? []
116128
}
117129

130+
var menuBar: AXUIElement? {
131+
try? copyValue(key: kAXMenuBarAttribute)
132+
}
133+
118134
var visibleChildren: [AXUIElement] {
119135
(try? copyValue(key: kAXVisibleChildrenAttribute)) ?? []
120136
}
121137

122-
func child(identifier: String) -> AXUIElement? {
138+
func child(
139+
identifier: String? = nil,
140+
title: String? = nil,
141+
role: String? = nil
142+
) -> AXUIElement? {
123143
for child in children {
124-
if child.identifier == identifier { return child }
125-
if let target = child.child(identifier: identifier) { return target }
144+
let match = {
145+
if let identifier, child.identifier != identifier { return false }
146+
if let title, child.title != title { return false }
147+
if let role, child.role != role { return false }
148+
return true
149+
}()
150+
if match { return child }
151+
}
152+
for child in children {
153+
if let target = child.child(
154+
identifier: identifier,
155+
title: title,
156+
role: role
157+
) { return target }
126158
}
127159
return nil
128160
}

Core/Sources/CopilotService/CopilotService.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CopilotModel
22
import Foundation
33
import LanguageClient
44
import LanguageServerProtocol
5+
import Logger
56
import Preferences
67
import XPCShared
78

@@ -25,10 +26,15 @@ public protocol CopilotSuggestionServiceType {
2526
) async throws -> [CopilotCompletion]
2627
func notifyAccepted(_ completion: CopilotCompletion) async
2728
func notifyRejected(_ completions: [CopilotCompletion]) async
29+
func notifyOpenTextDocument(fileURL: URL, content: String) async throws
30+
func notifyChangeTextDocument(fileURL: URL, content: String) async throws
31+
func notifyCloseTextDocument(fileURL: URL) async throws
32+
func notifySaveTextDocument(fileURL: URL) async throws
2833
}
2934

3035
protocol CopilotLSP {
3136
func sendRequest<E: CopilotRequestType>(_ endpoint: E) async throws -> E.Response
37+
func sendNotification(_ notif: ClientNotification) async throws
3238
}
3339

3440
public class CopilotBaseService {
@@ -57,7 +63,7 @@ public class CopilotBaseService {
5763
}
5864
let executionParams: Process.ExecutionParameters
5965
let runner = UserDefaults.shared.value(for: \.runNodeWith)
60-
66+
6167
switch runner {
6268
case .bash:
6369
let nodePath = UserDefaults.shared.value(for: \.nodePath)
@@ -248,6 +254,57 @@ public final class CopilotSuggestionService: CopilotBaseService, CopilotSuggesti
248254
CopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.uuid))
249255
)
250256
}
257+
258+
public func notifyOpenTextDocument(
259+
fileURL: URL,
260+
content: String
261+
) async throws {
262+
let languageId = languageIdentifierFromFileURL(fileURL)
263+
let uri = "file://\(fileURL.path)"
264+
// Logger.service.debug("Open \(uri)")
265+
try await server.sendNotification(
266+
.didOpenTextDocument(
267+
DidOpenTextDocumentParams(
268+
textDocument: .init(
269+
uri: uri,
270+
languageId: languageId.rawValue,
271+
version: 0,
272+
text: content
273+
)
274+
)
275+
)
276+
)
277+
}
278+
279+
public func notifyChangeTextDocument(fileURL: URL, content: String) async throws {
280+
let uri = "file://\(fileURL.path)"
281+
// Logger.service.debug("Change \(uri)")
282+
try await server.sendNotification(
283+
.didChangeTextDocument(
284+
DidChangeTextDocumentParams(
285+
uri: uri,
286+
version: 0,
287+
contentChange: .init(
288+
range: nil,
289+
rangeLength: nil,
290+
text: content
291+
)
292+
)
293+
)
294+
)
295+
}
296+
297+
public func notifySaveTextDocument(fileURL: URL) async throws {
298+
let uri = "file://\(fileURL.path)"
299+
// Logger.service.debug("Save \(uri)")
300+
try await server.sendNotification(.didSaveTextDocument(.init(uri: uri)))
301+
}
302+
303+
public func notifyCloseTextDocument(fileURL: URL) async throws {
304+
let uri = "file://\(fileURL.path)"
305+
// Logger.service.debug("Close \(uri)")
306+
try await server.sendNotification(.didCloseTextDocument(.init(uri: uri)))
307+
}
251308
}
252309

253310
extension InitializingServer: CopilotLSP {

Core/Sources/Environment/Environment.swift

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AppKit
33
import AXExtension
44
import CopilotService
55
import Foundation
6+
import Logger
67

78
public struct NoAccessToAccessibilityAPIError: Error, LocalizedError {
89
public var errorDescription: String? {
@@ -104,6 +105,24 @@ public enum Environment {
104105
}
105106
throw FailedToFetchFileURLError()
106107
}
108+
109+
public static var fetchFocusedElementURI: () async throws -> URL = {
110+
guard let xcode = ActiveApplicationMonitor.activeXcode
111+
?? ActiveApplicationMonitor.latestXcode
112+
else {
113+
throw FailedToFetchFileURLError()
114+
}
115+
116+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
117+
let focusedElement = application.focusedElement
118+
if focusedElement?.description != "Source Editor" {
119+
let window = application.focusedWindow
120+
let id = window?.identifier.hashValue
121+
return URL(fileURLWithPath: "/xcode-focused-element/\(id ?? 0)")
122+
}
123+
124+
return try await fetchCurrentFileURL()
125+
}
107126

108127
public static var createAuthService: () -> CopilotAuthServiceType = {
109128
CopilotAuthService()
@@ -120,27 +139,71 @@ public enum Environment {
120139
else { return }
121140
let bundleName = Bundle.main
122141
.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
142+
143+
await Task.yield()
123144

124-
/// check if menu is open, if not, click the menu item.
125-
let appleScript = """
126-
tell application "System Events"
127-
set theprocs to every process whose unix id is \(activeXcode.processIdentifier)
128-
repeat with proc in theprocs
129-
set the frontmost of proc to true
130-
tell proc
131-
repeat with theMenu in menus of menu bar 1
132-
set theValue to value of attribute "AXVisibleChildren" of theMenu
133-
if theValue is not {} then
134-
return
135-
end if
136-
end repeat
137-
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
138-
end tell
139-
end repeat
140-
end tell
141-
"""
145+
if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) {
146+
if !activeXcode.isActive { activeXcode.activate() }
147+
let app = AXUIElementCreateApplication(activeXcode.processIdentifier)
142148

143-
try await runAppleScript(appleScript)
149+
if let editorMenu = app.menuBar?.child(title: "Editor"),
150+
let commandMenu = editorMenu.child(title: bundleName)
151+
{
152+
if let button = commandMenu.child(title: name, role: "AXMenuItem") {
153+
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
154+
if error != AXError.success {
155+
Logger.service
156+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
157+
throw error
158+
}
159+
}
160+
} else if let commandMenu = app.menuBar?.child(title: bundleName),
161+
let button = commandMenu.child(title: name, role: "AXMenuItem")
162+
{
163+
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
164+
if error != AXError.success {
165+
Logger.service
166+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
167+
throw error
168+
}
169+
} else {
170+
struct CantRunCommand: Error, LocalizedError {
171+
let name: String
172+
var errorDescription: String? {
173+
"Can't run command \(name)."
174+
}
175+
}
176+
177+
throw CantRunCommand(name: name)
178+
}
179+
} else {
180+
/// check if menu is open, if not, click the menu item.
181+
let appleScript = """
182+
tell application "System Events"
183+
set theprocs to every process whose unix id is \(activeXcode.processIdentifier)
184+
repeat with proc in theprocs
185+
set the frontmost of proc to true
186+
tell proc
187+
repeat with theMenu in menus of menu bar 1
188+
set theValue to value of attribute "AXVisibleChildren" of theMenu
189+
if theValue is not {} then
190+
return
191+
end if
192+
end repeat
193+
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
194+
end tell
195+
end repeat
196+
end tell
197+
"""
198+
199+
do {
200+
try await runAppleScript(appleScript)
201+
} catch {
202+
Logger.service
203+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
204+
throw error
205+
}
206+
}
144207
}
145208

146209
public static var makeXcodeActive: () async throws -> Void = {

0 commit comments

Comments
 (0)