Skip to content

Commit 45326d8

Browse files
committed
Merge branch 'feature/project-root-url-new-method' into develop
2 parents a20c54c + 22a1d2f commit 45326d8

4 files changed

Lines changed: 160 additions & 161 deletions

File tree

Core/Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ let package = Package(
6868
"AXNotificationStream",
6969
"Environment",
7070
"SuggestionWidget",
71+
"AXExtension",
7172
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
7273
]
7374
),
@@ -93,7 +94,7 @@ let package = Package(
9394
.target(name: "AXNotificationStream"),
9495
.target(
9596
name: "Environment",
96-
dependencies: ["ActiveApplicationMonitor", "CopilotService"]
97+
dependencies: ["ActiveApplicationMonitor", "CopilotService", "AXExtension"]
9798
),
9899
.target(
99100
name: "SuggestionWidget",
@@ -104,5 +105,6 @@ let package = Package(
104105
]
105106
),
106107
.target(name: "UpdateChecker"),
108+
.target(name: "AXExtension"),
107109
]
108110
)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import AppKit
2+
import Foundation
3+
4+
public extension AXUIElement {
5+
var identifier: String {
6+
(try? copyValue(key: kAXIdentifierAttribute)) ?? ""
7+
}
8+
9+
var value: String {
10+
(try? copyValue(key: kAXValueAttribute)) ?? ""
11+
}
12+
13+
var document: String? {
14+
try? copyValue(key: kAXDocumentAttribute)
15+
}
16+
17+
var focusedElement: AXUIElement? {
18+
try? copyValue(key: kAXFocusedUIElementAttribute)
19+
}
20+
21+
var description: String {
22+
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
23+
}
24+
25+
var selectedTextRange: Range<Int>? {
26+
guard let value: AXValue = try? copyValue(key: kAXSelectedTextRangeAttribute)
27+
else { return nil }
28+
var range: CFRange = .init(location: 0, length: 0)
29+
if AXValueGetValue(value, .cfRange, &range) {
30+
return Range(.init(location: range.location, length: range.length))
31+
}
32+
return nil
33+
}
34+
35+
var sharedFocusElements: [AXUIElement] {
36+
(try? copyValue(key: kAXChildrenAttribute)) ?? []
37+
}
38+
39+
var window: AXUIElement? {
40+
try? copyValue(key: kAXWindowAttribute)
41+
}
42+
43+
var windows: [AXUIElement] {
44+
(try? copyValue(key: kAXWindowsAttribute)) ?? []
45+
}
46+
47+
var focusedWindow: AXUIElement? {
48+
try? copyValue(key: kAXFocusedWindowAttribute)
49+
}
50+
51+
var topLevelElement: AXUIElement? {
52+
try? copyValue(key: kAXTopLevelUIElementAttribute)
53+
}
54+
55+
var rows: [AXUIElement] {
56+
(try? copyValue(key: kAXRowsAttribute)) ?? []
57+
}
58+
59+
var parent: AXUIElement? {
60+
try? copyValue(key: kAXParentAttribute)
61+
}
62+
63+
var children: [AXUIElement] {
64+
(try? copyValue(key: kAXChildrenAttribute)) ?? []
65+
}
66+
67+
var visibleChildren: [AXUIElement] {
68+
(try? copyValue(key: kAXVisibleChildrenAttribute)) ?? []
69+
}
70+
71+
var isFocused: Bool {
72+
(try? copyValue(key: kAXFocusedAttribute)) ?? false
73+
}
74+
75+
var isEnabled: Bool {
76+
(try? copyValue(key: kAXEnabledAttribute)) ?? false
77+
}
78+
79+
func child(identifier: String) -> AXUIElement? {
80+
for child in children {
81+
if child.identifier == identifier { return child }
82+
if let target = child.child(identifier: identifier) { return target }
83+
}
84+
return nil
85+
}
86+
87+
func visibleChild(identifier: String) -> AXUIElement? {
88+
for child in visibleChildren {
89+
if child.identifier == identifier { return child }
90+
if let target = child.visibleChild(identifier: identifier) { return target }
91+
}
92+
return nil
93+
}
94+
95+
func copyValue<T>(key: String, ofType _: T.Type = T.self) throws -> T {
96+
var value: AnyObject?
97+
let error = AXUIElementCopyAttributeValue(self, key as CFString, &value)
98+
if error == .success, let value = value as? T {
99+
return value
100+
}
101+
throw error
102+
}
103+
104+
func copyParameterizedValue<T>(
105+
key: String,
106+
parameters: AnyObject,
107+
ofType _: T.Type = T.self
108+
) throws -> T {
109+
var value: AnyObject?
110+
let error = AXUIElementCopyParameterizedAttributeValue(
111+
self,
112+
key as CFString,
113+
parameters as CFTypeRef,
114+
&value
115+
)
116+
if error == .success, let value = value as? T {
117+
return value
118+
}
119+
throw error
120+
}
121+
122+
}
123+
124+
extension AXError: Error {}

Core/Sources/Environment/Environment.swift

Lines changed: 29 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ActiveApplicationMonitor
22
import AppKit
3+
import AXExtension
34
import CopilotService
45
import Foundation
56

@@ -40,28 +41,24 @@ public enum Environment {
4041
}
4142
}
4243

43-
#warning("""
44-
TODO: The current version causes an issue in real-time suggestion that when completion panel is open,
45-
the command handler can not find the correct workspace.
46-
""")
4744
public static var fetchCurrentProjectRootURL: (_ fileURL: URL?) async throws
4845
-> URL? = { fileURL in
49-
let appleScript = """
50-
tell application "Xcode"
51-
return path of document of the first window
52-
end tell
53-
"""
54-
55-
let path = (try? await runAppleScript(appleScript)) ?? ""
56-
if !path.isEmpty {
57-
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
58-
var url = URL(fileURLWithPath: trimmedNewLine)
59-
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
60-
!url.pathExtension.isEmpty
61-
{
62-
url = url.deletingLastPathComponent()
46+
if let xcode = ActiveApplicationMonitor.activeXcode {
47+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
48+
let focusedWindow = application.focusedWindow
49+
for child in focusedWindow?.children ?? [] {
50+
if child.description.starts(with: "/") {
51+
let path = child.description
52+
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
53+
var url = URL(fileURLWithPath: trimmedNewLine)
54+
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
55+
!url.pathExtension.isEmpty
56+
{
57+
url = url.deletingLastPathComponent()
58+
}
59+
return url
60+
}
6361
}
64-
return url
6562
}
6663

6764
guard var currentURL = fileURL else { return nil }
@@ -86,31 +83,21 @@ public enum Environment {
8683

8784
// fetch file path of the frontmost window of Xcode through Accessability API.
8885
let application = AXUIElementCreateApplication(xcode.processIdentifier)
89-
do {
90-
let frontmostWindow: AXUIElement = try application
91-
.copyValue(key: kAXFocusedWindowAttribute)
92-
var path: String? = try? frontmostWindow.copyValue(key: kAXDocumentAttribute)
93-
if path == nil {
94-
for window in try application.copyValue(
95-
key: kAXWindowsAttribute,
96-
ofType: [AXUIElement].self
97-
) {
98-
path = try? window.copyValue(key: kAXDocumentAttribute)
99-
if path != nil { break }
100-
}
101-
}
102-
if let path = path?.removingPercentEncoding {
103-
let url = URL(
104-
fileURLWithPath: path
105-
.replacingOccurrences(of: "file://", with: "")
106-
)
107-
return url
108-
}
109-
} catch {
110-
if let axError = error as? AXError, axError == .apiDisabled {
111-
throw NoAccessToAccessibilityAPIError()
86+
let focusedWindow = application.focusedWindow
87+
var path = focusedWindow?.document
88+
if path == nil {
89+
for window in application.windows {
90+
path = window.document
91+
if path != nil { break }
11292
}
11393
}
94+
if let path = path?.removingPercentEncoding {
95+
let url = URL(
96+
fileURLWithPath: path
97+
.replacingOccurrences(of: "file://", with: "")
98+
)
99+
return url
100+
}
114101
throw FailedToFetchFileURLError()
115102
}
116103

@@ -158,37 +145,6 @@ public enum Environment {
158145
}
159146
}
160147

161-
extension AXError: Error {}
162-
163-
public extension AXUIElement {
164-
func copyValue<T>(key: String, ofType _: T.Type = T.self) throws -> T {
165-
var value: AnyObject?
166-
let error = AXUIElementCopyAttributeValue(self, key as CFString, &value)
167-
if error == .success, let value = value as? T {
168-
return value
169-
}
170-
throw error
171-
}
172-
173-
func copyParameterizedValue<T>(
174-
key: String,
175-
parameters: AnyObject,
176-
ofType _: T.Type = T.self
177-
) throws -> T {
178-
var value: AnyObject?
179-
let error = AXUIElementCopyParameterizedAttributeValue(
180-
self,
181-
key as CFString,
182-
parameters as CFTypeRef,
183-
&value
184-
)
185-
if error == .success, let value = value as? T {
186-
return value
187-
}
188-
throw error
189-
}
190-
}
191-
192148
@discardableResult
193149
func runAppleScript(_ appleScript: String) async throws -> String {
194150
let task = Process()

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ActiveApplicationMonitor
22
import AppKit
33
import AsyncAlgorithms
4+
import AXExtension
45
import AXNotificationStream
56
import CGEventObserver
67
import Environment
@@ -166,7 +167,7 @@ public class RealtimeSuggestionController {
166167
#warning(
167168
"TODO: Any method to avoid using AppleScript to check that completion panel is presented?"
168169
)
169-
if await Environment.frontmostXcodeWindowIsEditor() {
170+
if isCommentMode, await Environment.frontmostXcodeWindowIsEditor() {
170171
if Task.isCancelled { return }
171172
self.triggerPrefetchDebounced(force: true)
172173
}
@@ -190,8 +191,8 @@ public class RealtimeSuggestionController {
190191
if Task.isCancelled { return }
191192

192193
os_log(.info, "Prefetch suggestions.")
193-
194-
if !force, await !Environment.frontmostXcodeWindowIsEditor() {
194+
195+
if !force, isCommentMode, await !Environment.frontmostXcodeWindowIsEditor() {
195196
os_log(.info, "Completion panel is open, blocked.")
196197
return
197198
}
@@ -235,87 +236,3 @@ public class RealtimeSuggestionController {
235236
return application.focusedWindow?.child(identifier: "_XC_COMPLETION_TABLE_") != nil
236237
}
237238
}
238-
239-
extension AXUIElement {
240-
var identifier: String {
241-
(try? copyValue(key: kAXIdentifierAttribute)) ?? ""
242-
}
243-
244-
var value: String {
245-
(try? copyValue(key: kAXValueAttribute)) ?? ""
246-
}
247-
248-
var focusedElement: AXUIElement? {
249-
try? copyValue(key: kAXFocusedUIElementAttribute)
250-
}
251-
252-
var description: String {
253-
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
254-
}
255-
256-
var selectedTextRange: Range<Int>? {
257-
guard let value: AXValue = try? copyValue(key: kAXSelectedTextRangeAttribute)
258-
else { return nil }
259-
var range: CFRange = .init(location: 0, length: 0)
260-
if AXValueGetValue(value, .cfRange, &range) {
261-
return Range(.init(location: range.location, length: range.length))
262-
}
263-
return nil
264-
}
265-
266-
var sharedFocusElements: [AXUIElement] {
267-
(try? copyValue(key: kAXChildrenAttribute)) ?? []
268-
}
269-
270-
var window: AXUIElement? {
271-
try? copyValue(key: kAXWindowAttribute)
272-
}
273-
274-
var focusedWindow: AXUIElement? {
275-
try? copyValue(key: kAXFocusedWindowAttribute)
276-
}
277-
278-
var topLevelElement: AXUIElement? {
279-
try? copyValue(key: kAXTopLevelUIElementAttribute)
280-
}
281-
282-
var rows: [AXUIElement] {
283-
(try? copyValue(key: kAXRowsAttribute)) ?? []
284-
}
285-
286-
var parent: AXUIElement? {
287-
try? copyValue(key: kAXParentAttribute)
288-
}
289-
290-
var children: [AXUIElement] {
291-
(try? copyValue(key: kAXChildrenAttribute)) ?? []
292-
}
293-
294-
var visibleChildren: [AXUIElement] {
295-
(try? copyValue(key: kAXVisibleChildrenAttribute)) ?? []
296-
}
297-
298-
var isFocused: Bool {
299-
(try? copyValue(key: kAXFocusedAttribute)) ?? false
300-
}
301-
302-
var isEnabled: Bool {
303-
(try? copyValue(key: kAXEnabledAttribute)) ?? false
304-
}
305-
306-
func child(identifier: String) -> AXUIElement? {
307-
for child in children {
308-
if child.identifier == identifier { return child }
309-
if let target = child.child(identifier: identifier) { return target }
310-
}
311-
return nil
312-
}
313-
314-
func visibleChild(identifier: String) -> AXUIElement? {
315-
for child in visibleChildren {
316-
if child.identifier == identifier { return child }
317-
if let target = child.visibleChild(identifier: identifier) { return target }
318-
}
319-
return nil
320-
}
321-
}

0 commit comments

Comments
 (0)