Skip to content

Commit f8c9ffb

Browse files
committed
Move the widget controller to its own package
1 parent 95cc220 commit f8c9ffb

20 files changed

+413
-189
lines changed

Core/Package.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ let package = Package(
6060
"DisplayLink",
6161
"ActiveApplicationMonitor",
6262
"AXNotificationStream",
63+
"Environment",
64+
"SuggestionWidget",
6365
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
6466
]
6567
),
@@ -69,12 +71,31 @@ let package = Package(
6971
),
7072
.testTarget(
7173
name: "ServiceTests",
72-
dependencies: ["Service", "Client", "CopilotService", "SuggestionInjector", "XPCShared"]
74+
dependencies: [
75+
"Service",
76+
"Client",
77+
"CopilotService",
78+
"SuggestionInjector",
79+
"XPCShared",
80+
"Environment",
81+
]
7382
),
7483
.target(name: "FileChangeChecker"),
7584
.target(name: "LaunchAgentManager"),
7685
.target(name: "DisplayLink"),
7786
.target(name: "ActiveApplicationMonitor"),
7887
.target(name: "AXNotificationStream"),
88+
.target(
89+
name: "Environment",
90+
dependencies: ["ActiveApplicationMonitor", "CopilotService"]
91+
),
92+
.target(
93+
name: "SuggestionWidget",
94+
dependencies: [
95+
"ActiveApplicationMonitor",
96+
"AXNotificationStream",
97+
"Environment",
98+
]
99+
),
79100
]
80101
)

Core/Sources/Service/Environment.swift renamed to Core/Sources/Environment/Environment.swift

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ import AppKit
33
import CopilotService
44
import Foundation
55

6-
struct NoAccessToAccessibilityAPIError: Error, LocalizedError {
7-
var errorDescription: String? {
6+
public struct NoAccessToAccessibilityAPIError: Error, LocalizedError {
7+
public var errorDescription: String? {
88
"Accessibility API permission is not granted. Please enable in System Settings.app."
99
}
10+
public init() {}
1011
}
1112

12-
struct FailedToFetchFileURLError: Error, LocalizedError {
13-
var errorDescription: String? {
13+
public struct FailedToFetchFileURLError: Error, LocalizedError {
14+
public var errorDescription: String? {
1415
"Failed to fetch editing file url."
1516
}
17+
public init() {}
1618
}
1719

18-
enum Environment {
19-
static var now = { Date() }
20+
public enum Environment {
21+
public static var now = { Date() }
2022

21-
static var isXcodeActive: () async -> Bool = {
23+
public static var isXcodeActive: () async -> Bool = {
2224
ActiveApplicationMonitor.activeXcode != nil
2325
}
2426

25-
static var frontmostXcodeWindowIsEditor: () async -> Bool = {
27+
public static var frontmostXcodeWindowIsEditor: () async -> Bool = {
2628
let appleScript = """
2729
tell application "Xcode"
2830
return path of document of the first window
@@ -36,41 +38,42 @@ enum Environment {
3638
}
3739
}
3840

39-
static var fetchCurrentProjectRootURL: (_ fileURL: URL?) async throws -> URL? = { fileURL in
40-
let appleScript = """
41-
tell application "Xcode"
42-
return path of document of the first window
43-
end tell
44-
"""
45-
46-
let path = (try? await runAppleScript(appleScript)) ?? ""
47-
if !path.isEmpty {
48-
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
49-
var url = URL(fileURLWithPath: trimmedNewLine)
50-
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
51-
!url.pathExtension.isEmpty
52-
{
53-
url = url.deletingLastPathComponent()
41+
public static var fetchCurrentProjectRootURL: (_ fileURL: URL?) async throws
42+
-> URL? = { fileURL in
43+
let appleScript = """
44+
tell application "Xcode"
45+
return path of document of the first window
46+
end tell
47+
"""
48+
49+
let path = (try? await runAppleScript(appleScript)) ?? ""
50+
if !path.isEmpty {
51+
let trimmedNewLine = path.trimmingCharacters(in: .newlines)
52+
var url = URL(fileURLWithPath: trimmedNewLine)
53+
while !FileManager.default.fileIsDirectory(atPath: url.path) ||
54+
!url.pathExtension.isEmpty
55+
{
56+
url = url.deletingLastPathComponent()
57+
}
58+
return url
5459
}
55-
return url
56-
}
5760

58-
guard var currentURL = fileURL else { return nil }
59-
var firstDirectoryURL: URL?
60-
while currentURL.pathComponents.count > 1 {
61-
defer { currentURL.deleteLastPathComponent() }
62-
guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue }
63-
if firstDirectoryURL == nil { firstDirectoryURL = currentURL }
64-
let gitURL = currentURL.appendingPathComponent(".git")
65-
if FileManager.default.fileIsDirectory(atPath: gitURL.path) {
66-
return currentURL
61+
guard var currentURL = fileURL else { return nil }
62+
var firstDirectoryURL: URL?
63+
while currentURL.pathComponents.count > 1 {
64+
defer { currentURL.deleteLastPathComponent() }
65+
guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue }
66+
if firstDirectoryURL == nil { firstDirectoryURL = currentURL }
67+
let gitURL = currentURL.appendingPathComponent(".git")
68+
if FileManager.default.fileIsDirectory(atPath: gitURL.path) {
69+
return currentURL
70+
}
6771
}
68-
}
6972

70-
return firstDirectoryURL ?? fileURL
71-
}
73+
return firstDirectoryURL ?? fileURL
74+
}
7275

73-
static var fetchCurrentFileURL: () async throws -> URL = {
76+
public static var fetchCurrentFileURL: () async throws -> URL = {
7477
guard let xcode = ActiveApplicationMonitor.activeXcode else {
7578
throw FailedToFetchFileURLError()
7679
}
@@ -105,16 +108,16 @@ enum Environment {
105108
throw FailedToFetchFileURLError()
106109
}
107110

108-
static var createAuthService: () -> CopilotAuthServiceType = {
111+
public static var createAuthService: () -> CopilotAuthServiceType = {
109112
CopilotAuthService()
110113
}
111114

112-
static var createSuggestionService: (_ projectRootURL: URL)
115+
public static var createSuggestionService: (_ projectRootURL: URL)
113116
-> CopilotSuggestionServiceType = { projectRootURL in
114117
CopilotSuggestionService(projectRootURL: projectRootURL)
115118
}
116119

117-
static var triggerAction: (_ name: String) async throws -> Void = { name in
120+
public static var triggerAction: (_ name: String) async throws -> Void = { name in
118121
var xcodes = [NSRunningApplication]()
119122
var retryCount = 0
120123
// Sometimes runningApplications returns 0 items.
@@ -151,7 +154,7 @@ enum Environment {
151154

152155
extension AXError: Error {}
153156

154-
extension AXUIElement {
157+
public extension AXUIElement {
155158
func copyValue<T>(key: String, ofType _: T.Type = T.self) throws -> T {
156159
var value: AnyObject?
157160
let error = AXUIElementCopyAttributeValue(self, key as CFString, &value)
@@ -179,3 +182,42 @@ extension AXUIElement {
179182
throw error
180183
}
181184
}
185+
186+
@discardableResult
187+
func runAppleScript(_ appleScript: String) async throws -> String {
188+
let task = Process()
189+
task.launchPath = "/usr/bin/osascript"
190+
task.arguments = ["-e", appleScript]
191+
let outpipe = Pipe()
192+
task.standardOutput = outpipe
193+
task.standardError = Pipe()
194+
195+
return try await withUnsafeThrowingContinuation { continuation in
196+
do {
197+
task.terminationHandler = { _ in
198+
do {
199+
if let data = try outpipe.fileHandleForReading.readToEnd(),
200+
let content = String(data: data, encoding: .utf8)
201+
{
202+
continuation.resume(returning: content)
203+
return
204+
}
205+
continuation.resume(returning: "")
206+
} catch {
207+
continuation.resume(throwing: error)
208+
}
209+
}
210+
try task.run()
211+
} catch {
212+
continuation.resume(throwing: error)
213+
}
214+
}
215+
}
216+
217+
extension FileManager {
218+
func fileIsDirectory(atPath path: String) -> Bool {
219+
var isDirectory: ObjCBool = false
220+
let exists = fileExists(atPath: path, isDirectory: &isDirectory)
221+
return isDirectory.boolValue && exists
222+
}
223+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import AppKit
2+
import SuggestionWidget
23

34
@MainActor
45
public final class GraphicalUserInterfaceController {
56
public nonisolated static let shared = GraphicalUserInterfaceController()
67
nonisolated let realtimeSuggestionIndicatorController = RealtimeSuggestionIndicatorController()
7-
nonisolated let suggestionPanelController = SuggestionPanelController()
8+
nonisolated let suggestionWidget = SuggestionWidgetController()
89
private nonisolated init() {}
910
}

Core/Sources/Service/GUI/RealtimeSuggestionIndicatorController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AppKit
22
import DisplayLink
3+
import Environment
34
import QuartzCore
45
import SwiftUI
56
import XPCShared

Core/Sources/Service/Helpers.swift

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,6 @@
11
import Foundation
22
import LanguageServerProtocol
33

4-
extension FileManager {
5-
func fileIsDirectory(atPath path: String) -> Bool {
6-
var isDirectory: ObjCBool = false
7-
let exists = fileExists(atPath: path, isDirectory: &isDirectory)
8-
return isDirectory.boolValue && exists
9-
}
10-
}
11-
12-
@discardableResult
13-
func runAppleScript(_ appleScript: String) async throws -> String {
14-
let task = Process()
15-
task.launchPath = "/usr/bin/osascript"
16-
task.arguments = ["-e", appleScript]
17-
let outpipe = Pipe()
18-
task.standardOutput = outpipe
19-
task.standardError = Pipe()
20-
21-
return try await withUnsafeThrowingContinuation { continuation in
22-
do {
23-
task.terminationHandler = { _ in
24-
do {
25-
if let data = try outpipe.fileHandleForReading.readToEnd(),
26-
let content = String(data: data, encoding: .utf8)
27-
{
28-
continuation.resume(returning: content)
29-
return
30-
}
31-
continuation.resume(returning: "")
32-
} catch {
33-
continuation.resume(throwing: error)
34-
}
35-
}
36-
try task.run()
37-
} catch {
38-
continuation.resume(throwing: error)
39-
}
40-
}
41-
}
42-
434
extension NSError {
445
static func from(_ error: Error) -> NSError {
456
if let error = error as? ServerError {

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ActiveApplicationMonitor
22
import AppKit
33
import CGEventObserver
4+
import Environment
45
import Foundation
56
import os.log
67
import XPCShared

Core/Sources/Service/SuggestionCommandHandler/CommentBaseCommandHandler.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CopilotModel
2+
import Environment
23
import Foundation
34
import SuggestionInjector
45
import XPCShared
@@ -9,7 +10,8 @@ struct CommentBaseCommandHandler: SuggestionCommandHanlder {
910

1011
func presentSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
1112
let fileURL = try await Environment.fetchCurrentFileURL()
12-
let (workspace, filespace) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
13+
let (workspace, filespace) = try await Workspace
14+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
1315
try await workspace.generateSuggestions(
1416
forFileAt: fileURL,
1517
content: editor.content,
@@ -32,7 +34,8 @@ struct CommentBaseCommandHandler: SuggestionCommandHanlder {
3234

3335
func presentNextSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
3436
let fileURL = try await Environment.fetchCurrentFileURL()
35-
let (workspace, filespace) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
37+
let (workspace, filespace) = try await Workspace
38+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
3639
workspace.selectNextSuggestion(
3740
forFileAt: fileURL,
3841
content: editor.content,
@@ -51,7 +54,8 @@ struct CommentBaseCommandHandler: SuggestionCommandHanlder {
5154

5255
func presentPreviousSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
5356
let fileURL = try await Environment.fetchCurrentFileURL()
54-
let (workspace, filespace) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
57+
let (workspace, filespace) = try await Workspace
58+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
5559
workspace.selectPreviousSuggestion(
5660
forFileAt: fileURL,
5761
content: editor.content,
@@ -70,7 +74,8 @@ struct CommentBaseCommandHandler: SuggestionCommandHanlder {
7074

7175
func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent? {
7276
let fileURL = try await Environment.fetchCurrentFileURL()
73-
let (workspace, filespace) = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
77+
let (workspace, filespace) = try await Workspace
78+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
7479
workspace.rejectSuggestion(forFileAt: fileURL)
7580

7681
let presenter = PresentInCommentSuggestionPresenter()

0 commit comments

Comments
 (0)