Skip to content

Commit 0216f3e

Browse files
committed
Installing GitHub Copilot LSP from GitHub
1 parent eb2756f commit 0216f3e

File tree

5 files changed

+156
-24
lines changed

5 files changed

+156
-24
lines changed

Core/Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,13 @@ let package = Package(
255255

256256
.target(
257257
name: "GitHubCopilotService",
258-
dependencies: ["LanguageClient", "SuggestionModel", "XPCShared", "Preferences"]
258+
dependencies: [
259+
"LanguageClient",
260+
"SuggestionModel",
261+
"XPCShared",
262+
"Preferences",
263+
"Terminal",
264+
]
259265
),
260266
.testTarget(
261267
name: "GitHubCopilotServiceTests",

Core/Sources/CodeiumService/CodeiumInstallationManager.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Terminal
33

44
public struct CodeiumInstallationManager {
55
private static var isInstalling = false
6-
static let latestSupportedVersion: String = "1.2.17"
6+
static let latestSupportedVersion = "1.2.17"
77

88
public init() {}
99

@@ -72,11 +72,12 @@ public struct CodeiumInstallationManager {
7272
try FileManager.default.copyItem(at: fileURL, to: targetURL)
7373
defer { try? FileManager.default.removeItem(at: targetURL) }
7474

75+
// uninstall
7576
continuation.yield(.uninstalling)
7677
try await uninstall()
7778

78-
continuation.yield(.decompressing)
7979
// extract file
80+
continuation.yield(.decompressing)
8081
let terminal = Terminal()
8182
_ = try await terminal.runCommand(
8283
"/usr/bin/gunzip",

Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,49 @@ import Terminal
44
public struct GitHubCopilotInstallationManager {
55
private static var isInstalling = false
66

7+
static var downloadURL: URL {
8+
let commitHash = "1358e8e45ecedc53daf971924a0541ddf6224faf"
9+
let link = "https://github.com/github/copilot.vim/archive/\(commitHash).zip"
10+
return URL(string: link)!
11+
}
12+
13+
static let latestSupportedVersion = "1.8.4"
14+
715
public init() {}
816

917
public enum InstallationStatus {
1018
case notInstalled
11-
case installed
19+
case installed(String)
20+
case outdated(current: String, latest: String)
21+
case unsupported(current: String, latest: String)
1222
}
1323

1424
public func checkInstallation() -> InstallationStatus {
1525
guard let urls = try? GitHubCopilotBaseService.createFoldersIfNeeded()
1626
else { return .notInstalled }
1727
let executableFolderURL = urls.executableURL
1828
let binaryURL = executableFolderURL.appendingPathComponent("copilot")
29+
let versionFileURL = executableFolderURL.appendingPathComponent("version")
1930

2031
if !FileManager.default.fileExists(atPath: binaryURL.path) {
2132
return .notInstalled
2233
}
2334

24-
return .installed
35+
if FileManager.default.fileExists(atPath: versionFileURL.path),
36+
let versionData = try? Data(contentsOf: versionFileURL),
37+
let version = String(data: versionData, encoding: .utf8)
38+
{
39+
switch version.compare(Self.latestSupportedVersion) {
40+
case .orderedAscending:
41+
return .outdated(current: version, latest: Self.latestSupportedVersion)
42+
case .orderedSame:
43+
return .installed(version)
44+
case .orderedDescending:
45+
return .unsupported(current: version, latest: Self.latestSupportedVersion)
46+
}
47+
}
48+
49+
return .outdated(current: "Unknown", latest: Self.latestSupportedVersion)
2550
}
2651

2752
public enum InstallationStep {
@@ -34,13 +59,16 @@ public struct GitHubCopilotInstallationManager {
3459
public enum Error: Swift.Error, LocalizedError {
3560
case isInstalling
3661
case failedToFindLanguageServer
62+
case failedToInstallLanguageServer
3763

3864
public var errorDescription: String? {
3965
switch self {
4066
case .isInstalling:
4167
return "Language server is installing."
4268
case .failedToFindLanguageServer:
4369
return "Failed to find language server. Please open an issue on GitHub."
70+
case .failedToInstallLanguageServer:
71+
return "Failed to install language server. Please open an issue on GitHub."
4472
}
4573
}
4674
}
@@ -57,22 +85,63 @@ public struct GitHubCopilotInstallationManager {
5785
do {
5886
continuation.yield(.downloading)
5987
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")
6688

67-
try FileManager.default.copyItem(
68-
at: executable,
69-
to: targetURL
89+
// download
90+
let (fileURL, _) = try await URLSession.shared.download(from: Self.downloadURL)
91+
let targetURL = urls.executableURL.appendingPathComponent("archive")
92+
.appendingPathExtension("zip")
93+
try FileManager.default.copyItem(at: fileURL, to: targetURL)
94+
defer { try? FileManager.default.removeItem(at: targetURL) }
95+
96+
// uninstall
97+
continuation.yield(.uninstalling)
98+
try await uninstall()
99+
100+
// decompress
101+
continuation.yield(.decompressing)
102+
let terminal = Terminal()
103+
104+
_ = try await terminal.runCommand(
105+
"/usr/bin/unzip",
106+
arguments: [targetURL.path],
107+
currentDirectoryPath: urls.executableURL.path,
108+
environment: [:]
70109
)
71110

111+
let contentURLs = try FileManager.default.contentsOfDirectory(
112+
at: urls.executableURL,
113+
includingPropertiesForKeys: nil,
114+
options: []
115+
)
116+
117+
defer {
118+
for url in contentURLs {
119+
try? FileManager.default.removeItem(at: url)
120+
}
121+
}
122+
123+
guard let gitFolderURL = contentURLs
124+
.first(where: { $0.lastPathComponent.hasPrefix("copilot.vim") })
125+
else {
126+
continuation.finish(throwing: Error.failedToInstallLanguageServer)
127+
return
128+
}
129+
130+
let lspURL = gitFolderURL.appendingPathComponent("copilot")
131+
let installationURL = urls.executableURL.appendingPathComponent("copilot")
132+
try FileManager.default.copyItem(at: lspURL, to: installationURL)
133+
72134
// update permission 755
73135
try FileManager.default.setAttributes(
74136
[.posixPermissions: 0o755],
75-
ofItemAtPath: targetURL.path
137+
ofItemAtPath: installationURL.path
138+
)
139+
140+
// create version file
141+
let data = Self.latestSupportedVersion.data(using: .utf8)
142+
FileManager.default.createFile(
143+
atPath: urls.executableURL.appendingPathComponent("version").path,
144+
contents: data
76145
)
77146

78147
continuation.yield(.done)
@@ -89,8 +158,13 @@ public struct GitHubCopilotInstallationManager {
89158
else { return }
90159
let executableFolderURL = urls.executableURL
91160
let binaryURL = executableFolderURL.appendingPathComponent("copilot")
161+
let versionFileURL = executableFolderURL.appendingPathComponent("version")
92162
if FileManager.default.fileExists(atPath: binaryURL.path) {
93163
try FileManager.default.removeItem(at: binaryURL)
94164
}
165+
if FileManager.default.fileExists(atPath: versionFileURL.path) {
166+
try FileManager.default.removeItem(at: versionFileURL)
167+
}
95168
}
96169
}
170+

Core/Sources/GitHubCopilotService/GitHubCopilotService.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ public class GitHubCopilotBaseService {
198198
try? FileManager.default
199199
.createDirectory(at: executableFolderURL, withIntermediateDirectories: false)
200200
}
201+
202+
try FileManager.default.setAttributes(
203+
[.posixPermissions: 0o755],
204+
ofItemAtPath: executableFolderURL.path
205+
)
206+
207+
try FileManager.default.setAttributes(
208+
[.posixPermissions: 0o755],
209+
ofItemAtPath: supportFolderURL.path
210+
)
201211

202212
return (supportURL, gitHubCopilotFolderURL, executableFolderURL, supportFolderURL)
203213
}

Core/Sources/HostApp/AccountSettings/CopilotView.swift

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ struct CopilotView: View {
1616

1717
init() {}
1818
}
19-
19+
2020
class ViewModel: ObservableObject {
2121
let installationManager = GitHubCopilotInstallationManager()
22-
22+
2323
@Published var installationStatus: GitHubCopilotInstallationManager.InstallationStatus
2424
@Published var installationStep: GitHubCopilotInstallationManager.InstallationStep?
25-
25+
2626
init() {
2727
installationStatus = installationManager.checkInstallation()
2828
}
29-
29+
3030
init(
3131
installationStatus: GitHubCopilotInstallationManager.InstallationStatus,
3232
installationStep: GitHubCopilotInstallationManager.InstallationStep?
@@ -35,13 +35,13 @@ struct CopilotView: View {
3535
self.installationStatus = installationStatus
3636
self.installationStep = installationStep
3737
}
38-
38+
3939
func refreshInstallationStatus() {
4040
Task { @MainActor in
4141
installationStatus = installationManager.checkInstallation()
4242
}
4343
}
44-
44+
4545
func install() async throws {
4646
defer { refreshInstallationStatus() }
4747
do {
@@ -92,7 +92,7 @@ struct CopilotView: View {
9292
Self.copilotAuthService = service
9393
return service
9494
}
95-
95+
9696
var installButton: some View {
9797
Button(action: {
9898
Task {
@@ -108,6 +108,21 @@ struct CopilotView: View {
108108
.disabled(viewModel.installationStep != nil)
109109
}
110110

111+
var updateButton: some View {
112+
Button(action: {
113+
Task {
114+
do {
115+
try await viewModel.install()
116+
} catch {
117+
toast(Text(error.localizedDescription), .error)
118+
}
119+
}
120+
}) {
121+
Text("Update")
122+
}
123+
.disabled(viewModel.installationStep != nil)
124+
}
125+
111126
var uninstallButton: some View {
112127
Button(action: {
113128
viewModel.uninstall()
@@ -150,14 +165,26 @@ struct CopilotView: View {
150165

151166
VStack(alignment: .leading) {
152167
HStack {
153-
Text("Language Server Version: \(version ?? "Loading..")")
154168
switch viewModel.installationStatus {
155169
case .notInstalled:
170+
Text("Copilot.Vim Version: Not Installed")
156171
installButton
157-
case .installed:
172+
case let .installed(version):
173+
Text("Copilot.Vim Version: \(version)")
174+
uninstallButton
175+
case let .outdated(version, latest):
176+
Text("Copilot.Vim Version: \(version) (Update Available: \(latest))")
177+
updateButton
178+
uninstallButton
179+
case let .unsupported(version, latest):
180+
Text("Copilot.Vim Version: \(version) (Supported Version: \(latest))")
181+
updateButton
158182
uninstallButton
159183
}
160184
}
185+
186+
Text("Language Server Version: \(version ?? "Loading..")")
187+
161188
Text("Status: \(status?.description ?? "Loading..")")
162189

163190
HStack(alignment: .center) {
@@ -208,6 +235,20 @@ struct CopilotView: View {
208235
Self.copilotAuthService = nil
209236
}.onChange(of: settings.nodePath) { _ in
210237
Self.copilotAuthService = nil
238+
}.onChange(of: viewModel.installationStep) { newValue in
239+
if let step = newValue {
240+
switch step {
241+
case .downloading:
242+
toast(Text("Downloading.."), .info)
243+
case .uninstalling:
244+
toast(Text("Uninstalling old version.."), .info)
245+
case .decompressing:
246+
toast(Text("Decompressing.."), .info)
247+
case .done:
248+
toast(Text("Done!"), .info)
249+
checkStatus()
250+
}
251+
}
211252
}
212253
}
213254

0 commit comments

Comments
 (0)