Skip to content

Commit 7bcc7f3

Browse files
committed
Merge branch 'release/0.15.3'
2 parents c364646 + 789c22f commit 7bcc7f3

File tree

9 files changed

+234
-22
lines changed

9 files changed

+234
-22
lines changed

Core/Sources/Environment/Environment.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,20 @@ public enum Environment {
116116

117117
let application = AXUIElementCreateApplication(xcode.processIdentifier)
118118
let focusedElement = application.focusedElement
119-
if focusedElement?.description != "Source Editor" {
119+
var windowElement: URL {
120120
let window = application.focusedWindow
121121
let id = window?.identifier.hashValue
122122
return URL(fileURLWithPath: "/xcode-focused-element/\(id ?? 0)")
123123
}
124+
if focusedElement?.description != "Source Editor" {
125+
return windowElement
126+
}
124127

125-
return try await fetchCurrentFileURL()
128+
do {
129+
return try await fetchCurrentFileURL()
130+
} catch {
131+
return windowElement
132+
}
126133
}
127134

128135
public static var createSuggestionService: (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
import Terminal
3+
4+
public struct GitHubCopilotInstallationManager {
5+
private static var isInstalling = false
6+
7+
public init() {}
8+
9+
public enum InstallationStatus {
10+
case notInstalled
11+
case installed
12+
}
13+
14+
public func checkInstallation() -> InstallationStatus {
15+
guard let urls = try? GitHubCopilotBaseService.createFoldersIfNeeded()
16+
else { return .notInstalled }
17+
let executableFolderURL = urls.executableURL
18+
let binaryURL = executableFolderURL.appendingPathComponent("copilot")
19+
20+
if !FileManager.default.fileExists(atPath: binaryURL.path) {
21+
return .notInstalled
22+
}
23+
24+
return .installed
25+
}
26+
27+
public enum InstallationStep {
28+
case downloading
29+
case uninstalling
30+
case decompressing
31+
case done
32+
}
33+
34+
public enum Error: Swift.Error, LocalizedError {
35+
case isInstalling
36+
case failedToFindLanguageServer
37+
38+
public var errorDescription: String? {
39+
switch self {
40+
case .isInstalling:
41+
return "Language server is installing."
42+
case .failedToFindLanguageServer:
43+
return "Failed to find language server. Please open an issue on GitHub."
44+
}
45+
}
46+
}
47+
48+
public func installLatestVersion() -> AsyncThrowingStream<InstallationStep, Swift.Error> {
49+
AsyncThrowingStream<InstallationStep, Swift.Error> { continuation in
50+
Task {
51+
guard !GitHubCopilotInstallationManager.isInstalling else {
52+
continuation.finish(throwing: Error.isInstalling)
53+
return
54+
}
55+
GitHubCopilotInstallationManager.isInstalling = true
56+
defer { GitHubCopilotInstallationManager.isInstalling = false }
57+
do {
58+
continuation.yield(.downloading)
59+
let urls = try GitHubCopilotBaseService.createFoldersIfNeeded()
60+
let executable = Bundle.main.bundleURL.appendingPathComponent("Contents/Applications/CopilotForXcodeExtensionService.app/Contents/Resources/copilot")
61+
guard FileManager.default.fileExists(atPath: executable.path) else {
62+
throw Error.failedToFindLanguageServer
63+
}
64+
65+
let targetURL = urls.executableURL.appendingPathComponent("copilot")
66+
67+
try FileManager.default.copyItem(
68+
at: executable,
69+
to: targetURL
70+
)
71+
72+
// update permission 755
73+
try FileManager.default.setAttributes(
74+
[.posixPermissions: 0o755],
75+
ofItemAtPath: targetURL.path
76+
)
77+
78+
continuation.yield(.done)
79+
continuation.finish()
80+
} catch {
81+
continuation.finish(throwing: error)
82+
}
83+
}
84+
}
85+
}
86+
87+
public func uninstall() async throws {
88+
guard let urls = try? GitHubCopilotBaseService.createFoldersIfNeeded()
89+
else { return }
90+
let executableFolderURL = urls.executableURL
91+
let binaryURL = executableFolderURL.appendingPathComponent("copilot")
92+
if FileManager.default.fileExists(atPath: binaryURL.path) {
93+
try FileManager.default.removeItem(at: binaryURL)
94+
}
95+
}
96+
}

Core/Sources/HostApp/AccountSettings/CopilotView.swift

+92-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,69 @@ struct CopilotView: View {
1616

1717
init() {}
1818
}
19+
20+
class ViewModel: ObservableObject {
21+
let installationManager = GitHubCopilotInstallationManager()
22+
23+
@Published var installationStatus: GitHubCopilotInstallationManager.InstallationStatus
24+
@Published var installationStep: GitHubCopilotInstallationManager.InstallationStep?
25+
26+
init() {
27+
installationStatus = installationManager.checkInstallation()
28+
}
29+
30+
init(
31+
installationStatus: GitHubCopilotInstallationManager.InstallationStatus,
32+
installationStep: GitHubCopilotInstallationManager.InstallationStep?
33+
) {
34+
assert(isPreview)
35+
self.installationStatus = installationStatus
36+
self.installationStep = installationStep
37+
}
38+
39+
func refreshInstallationStatus() {
40+
Task { @MainActor in
41+
installationStatus = installationManager.checkInstallation()
42+
}
43+
}
44+
45+
func install() async throws {
46+
defer { refreshInstallationStatus() }
47+
do {
48+
for try await step in installationManager.installLatestVersion() {
49+
Task { @MainActor in
50+
self.installationStep = step
51+
}
52+
}
53+
Task {
54+
try await Task.sleep(nanoseconds: 1_000_000_000)
55+
Task { @MainActor in
56+
self.installationStep = nil
57+
}
58+
}
59+
} catch {
60+
Task { @MainActor in
61+
installationStep = nil
62+
}
63+
throw error
64+
}
65+
}
66+
67+
func uninstall() {
68+
Task {
69+
defer { refreshInstallationStatus() }
70+
try await installationManager.uninstall()
71+
Task { @MainActor in
72+
CopilotView.copilotAuthService = nil
73+
}
74+
}
75+
}
76+
}
1977

2078
@Environment(\.openURL) var openURL
2179
@Environment(\.toast) var toast
2280
@StateObject var settings = Settings()
81+
@StateObject var viewModel = ViewModel()
2382

2483
@State var status: GitHubCopilotAccountStatus?
2584
@State var userCode: String?
@@ -33,6 +92,30 @@ struct CopilotView: View {
3392
Self.copilotAuthService = service
3493
return service
3594
}
95+
96+
var installButton: some View {
97+
Button(action: {
98+
Task {
99+
do {
100+
try await viewModel.install()
101+
} catch {
102+
toast(Text(error.localizedDescription), .error)
103+
}
104+
}
105+
}) {
106+
Text("Install")
107+
}
108+
.disabled(viewModel.installationStep != nil)
109+
}
110+
111+
var uninstallButton: some View {
112+
Button(action: {
113+
viewModel.uninstall()
114+
}) {
115+
Text("Uninstall")
116+
}
117+
.disabled(viewModel.installationStep != nil)
118+
}
36119

37120
var body: some View {
38121
HStack {
@@ -66,7 +149,15 @@ struct CopilotView: View {
66149
.foregroundColor(.secondary)
67150

68151
VStack(alignment: .leading) {
69-
Text("Language Server Version: \(version ?? "Loading..")")
152+
HStack {
153+
Text("Language Server Version: \(version ?? "Loading..")")
154+
switch viewModel.installationStatus {
155+
case .notInstalled:
156+
installButton
157+
case .installed:
158+
uninstallButton
159+
}
160+
}
70161
Text("Status: \(status?.description ?? "Loading..")")
71162

72163
HStack(alignment: .center) {

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift.swift

+5-12
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,12 @@ public final class GraphicalUserInterfaceController {
1010
private nonisolated init() {
1111
Task { @MainActor in
1212
suggestionWidget.dataSource = WidgetDataSource.shared
13-
suggestionWidget.onOpenChatClicked = {
13+
suggestionWidget.onOpenChatClicked = { [weak self] in
1414
Task {
15-
let commandHandler = WindowBaseCommandHandler()
16-
_ = try await commandHandler.chatWithSelection(editor: .init(
17-
content: "",
18-
lines: [],
19-
uti: "",
20-
cursorPosition: .outOfScope,
21-
selections: [],
22-
tabSize: 0,
23-
indentSize: 0,
24-
usesTabsForIndentation: false
25-
))
15+
let uri = try await Environment.fetchFocusedElementURI()
16+
let dataSource = WidgetDataSource.shared
17+
await dataSource.createChatIfNeeded(for: uri)
18+
self?.suggestionWidget.presentChatRoom(fileURL: uri)
2619
}
2720
}
2821
suggestionWidget.onCustomCommandClicked = { command in

Core/Sources/Service/GUI/WidgetDataSource.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@ final class WidgetDataSource {
4141
@discardableResult
4242
func createChatIfNeeded(for url: URL) -> ChatService {
4343
let build = {
44-
let service = ChatService(chatGPTService: ChatGPTService())
44+
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
45+
let systemPrompt = """
46+
\(language.isEmpty ? "" : "You must always reply in \(language)")
47+
You are a senior programmer, you will answer my questions concisely. If you are replying with code, embed the code in a code block in markdown.
48+
49+
You don't have any code in advance, ask me to provide it when needed.
50+
"""
51+
let service = ChatService(chatGPTService: ChatGPTService(systemPrompt: systemPrompt))
4552
let provider = ChatProvider(
4653
service: service,
4754
fileURL: url,

Core/Sources/SuggestionWidget/SuggestionWidgetController.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -555,14 +555,17 @@ extension SuggestionWidgetController {
555555
}
556556

557557
if let app = ActiveApplicationMonitor.activeApplication, app.isXcode {
558-
panelWindow.alphaValue = 1
559-
widgetWindow.alphaValue = 1
560-
tabWindow.alphaValue = 1
558+
let application = AXUIElementCreateApplication(app.processIdentifier)
559+
/// We need this to hide the windows when Xcode is minimized.
560+
let noFocus = application.focusedWindow == nil
561+
panelWindow.alphaValue = noFocus ? 0 : 1
562+
widgetWindow.alphaValue = noFocus ? 0 : 1
563+
tabWindow.alphaValue = noFocus ? 0 : 1
561564

562565
if detachChat {
563566
chatWindow.alphaValue = chatWindowViewModel.chat != nil ? 1 : 0
564567
} else {
565-
chatWindow.alphaValue = 1
568+
chatWindow.alphaValue = noFocus ? 0 : 1
566569
}
567570
} else if let app = ActiveApplicationMonitor.activeApplication,
568571
app.bundleIdentifier == Bundle.main.bundleIdentifier

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ brew upgrade --cask copilot-for-xcode
133133

134134
Alternatively, You can use the in-app updater or download the latest version manually from the latest [release](https://github.com/intitni/CopilotForXcode/releases).
135135

136+
After updating, please restart Xcode to allow the extension to reload.
137+
136138
If you are upgrading from a version lower than **0.7.0**, please run `Copilot for Xcode.app` at least once to let it set up the new launch agent for you and re-grant the permissions according to the new rules.
137139

138140
If you find that some of the features are no longer working, please first try regranting permissions to the app.

Version.xcconfig

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
APP_VERSION = 0.15.2
2-
APP_BUILD = 152
1+
APP_VERSION = 0.15.3
2+
APP_BUILD = 153

appcast.xml

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
33
<channel>
44
<title>Copilot for Xcode</title>
5+
6+
<item>
7+
<title>0.15.3</title>
8+
<pubDate>Mon, 15 May 2023 18:46:47 +0800</pubDate>
9+
<sparkle:version>153</sparkle:version>
10+
<sparkle:shortVersionString>0.15.3</sparkle:shortVersionString>
11+
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
12+
<sparkle:releaseNotesLink>
13+
https://github.com/intitni/CopilotForXcode/releases/tag/0.15.3
14+
</sparkle:releaseNotesLink>
15+
<enclosure url="https://github.com/intitni/CopilotForXcode/releases/download/0.15.3/Copilot.for.Xcode.app.zip" length="21342371" type="application/octet-stream" sparkle:edSignature="YYOBKinz8Ay++VYFIOwFRi4xcM3WuOTmPG6n4ar8e7Nc8Sq/JPLWaR2b6wLU05No2DCzMxlHXCF+a4a+o5V4AQ=="/>
16+
</item>
17+
518
<item>
619
<title>0.15.2</title>
720
<pubDate>Sun, 14 May 2023 20:58:20 +0800</pubDate>

0 commit comments

Comments
 (0)