Skip to content

Commit e814d65

Browse files
committed
Merge branch 'feature/integrating-codeium' into develop
2 parents 1ed614d + b14f98d commit e814d65

20 files changed

Lines changed: 1320 additions & 126 deletions

Copilot for Xcode/SettingsView.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct SettingsView: View {
3131
@AppStorage(\.suggestionCodeFontSize) var suggestionCodeFontSize
3232
@AppStorage(\.chatFontSize) var chatFontSize
3333
@AppStorage(\.chatCodeFontSize) var chatCodeFontSize
34+
@AppStorage(\.suggestionFeatureProvider) var suggestionFeatureProvider
3435
init() {}
3536
}
3637

@@ -79,6 +80,19 @@ struct SettingsView: View {
7980
} label: {
8081
Text("Present suggestions in")
8182
}
83+
84+
Picker(selection: $settings.suggestionFeatureProvider) {
85+
ForEach(SuggestionFeatureProvider.allCases, id: \.rawValue) {
86+
switch $0 {
87+
case .gitHubCopilot:
88+
Text("GitHub Copilot").tag($0)
89+
case .codeium:
90+
Text("Codeium").tag($0)
91+
}
92+
}
93+
} label: {
94+
Text("Generate suggestion with")
95+
}
8296

8397
if settings.suggestionPresentationMode == PresentationMode.floatingWidget {
8498
Picker(selection: $settings.suggestionWidgetPositionMode) {

Core/Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let package = Package(
1616
"LaunchAgentManager",
1717
"UpdateChecker",
1818
"Logger",
19+
"UserDefaultsObserver",
1920
]
2021
),
2122
.library(
@@ -69,6 +70,7 @@ let package = Package(
6970
"ChatService",
7071
"PromptToCodeService",
7172
"ServiceUpdateMigration",
73+
"UserDefaultsObserver",
7274
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
7375
]
7476
),
@@ -117,6 +119,8 @@ let package = Package(
117119
),
118120
.target(name: "SuggestionService", dependencies: [
119121
"GitHubCopilotService",
122+
"CodeiumService",
123+
"UserDefaultsObserver",
120124
]),
121125

122126
// MARK: - Prompt To Code
@@ -149,6 +153,7 @@ let package = Package(
149153
"Environment",
150154
"Highlightr",
151155
"Splash",
156+
"UserDefaultsObserver",
152157
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
153158
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
154159
]
@@ -178,6 +183,7 @@ let package = Package(
178183
name: "ServiceUpdateMigration",
179184
dependencies: ["Preferences", "GitHubCopilotService"]
180185
),
186+
.target(name: "UserDefaultsObserver"),
181187

182188
// MARK: - GitHub Copilot
183189

@@ -205,6 +211,13 @@ let package = Package(
205211
name: "OpenAIServiceTests",
206212
dependencies: ["OpenAIService"]
207213
),
214+
215+
// MARK: - Codeium
216+
217+
.target(
218+
name: "CodeiumService",
219+
dependencies: ["LanguageClient", "SuggestionModel", "Preferences"]
220+
),
208221
]
209222
)
210223

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import Foundation
2+
import JSONRPC
3+
import LanguageClient
4+
import LanguageServerProtocol
5+
import Logger
6+
7+
protocol CodeiumLSP {
8+
func sendRequest<E: CodeiumRequestType>(_ endpoint: E) async throws -> E.Response
9+
}
10+
11+
final class CodeiumLanguageServer {
12+
let languageServerExecutableURL: URL
13+
let managerDirectoryURL: URL
14+
let supportURL: URL
15+
let process: Process
16+
let transport: IOTransport
17+
var terminationHandler: (() -> Void)?
18+
var launchHandler: (() -> Void)?
19+
var port: String?
20+
var heartbeatTask: Task<Void, Error>?
21+
22+
init(
23+
languageServerExecutableURL: URL,
24+
managerDirectoryURL: URL,
25+
supportURL: URL,
26+
terminationHandler: (() -> Void)? = nil,
27+
launchHandler: (() -> Void)? = nil
28+
) {
29+
self.languageServerExecutableURL = languageServerExecutableURL
30+
self.managerDirectoryURL = managerDirectoryURL
31+
self.supportURL = supportURL
32+
self.terminationHandler = terminationHandler
33+
self.launchHandler = launchHandler
34+
process = Process()
35+
transport = IOTransport()
36+
37+
process.standardInput = transport.stdinPipe
38+
process.standardOutput = transport.stdoutPipe
39+
process.standardError = transport.stderrPipe
40+
41+
process.executableURL = languageServerExecutableURL
42+
43+
process.arguments = [
44+
"--api_server_host",
45+
"server.codeium.com",
46+
"--api_server_port",
47+
"443",
48+
"--manager_dir",
49+
managerDirectoryURL.path,
50+
]
51+
52+
process.currentDirectoryURL = supportURL
53+
54+
process.terminationHandler = { [weak self] task in
55+
self?.processTerminated(task)
56+
}
57+
}
58+
59+
func start() {
60+
guard !process.isRunning else { return }
61+
port = nil
62+
do {
63+
try process.run()
64+
65+
Task {
66+
func findPort() -> String? {
67+
// find a file in managerDirectoryURL whose name looks like a port, return the
68+
// name if found
69+
let fileManager = FileManager.default
70+
let enumerator = fileManager.enumerator(
71+
at: managerDirectoryURL,
72+
includingPropertiesForKeys: nil
73+
)
74+
while let fileURL = enumerator?.nextObject() as? URL {
75+
if fileURL.lastPathComponent.range(
76+
of: #"^\d+$"#,
77+
options: .regularExpression
78+
) != nil {
79+
return fileURL.lastPathComponent
80+
}
81+
}
82+
return nil
83+
}
84+
85+
try await Task.sleep(nanoseconds: 2_000_000)
86+
port = findPort()
87+
var waited = 0
88+
89+
while true {
90+
try await Task.sleep(nanoseconds: 1_000_000_000)
91+
waited += 1
92+
if let port = findPort() {
93+
finishStarting(port: port)
94+
return
95+
}
96+
if waited >= 60 {
97+
process.terminate()
98+
}
99+
}
100+
}
101+
} catch {
102+
Logger.codeium.error(error.localizedDescription)
103+
processTerminated(process)
104+
}
105+
}
106+
107+
deinit {
108+
process.terminationHandler = nil
109+
process.terminate()
110+
transport.close()
111+
}
112+
113+
private func processTerminated(_: Process) {
114+
transport.close()
115+
terminationHandler?()
116+
}
117+
118+
private func finishStarting(port: String) {
119+
Logger.codeium.info("Language server started.")
120+
self.port = port
121+
launchHandler?()
122+
}
123+
}
124+
125+
extension CodeiumLanguageServer: CodeiumLSP {
126+
func sendRequest<E>(_ request: E) async throws -> E.Response where E: CodeiumRequestType {
127+
guard let port else { throw CancellationError() }
128+
129+
let request = request.makeURLRequest(server: "http://127.0.0.1:\(port)")
130+
let (data, response) = try await URLSession.shared.data(for: request)
131+
if (response as? HTTPURLResponse)?.statusCode == 200 {
132+
do {
133+
let response = try JSONDecoder().decode(E.Response.self, from: data)
134+
return response
135+
} catch {
136+
dump(error)
137+
Logger.codeium.error(error.localizedDescription)
138+
throw error
139+
}
140+
} else {
141+
do {
142+
let error = try JSONDecoder().decode(CodeiumResponseError.self, from: data)
143+
throw error
144+
} catch {
145+
Logger.codeium.error(error.localizedDescription)
146+
throw error
147+
}
148+
}
149+
}
150+
}
151+
152+
final class IOTransport {
153+
public let stdinPipe: Pipe
154+
public let stdoutPipe: Pipe
155+
public let stderrPipe: Pipe
156+
private var closed: Bool
157+
private var queue: DispatchQueue
158+
159+
public init() {
160+
stdinPipe = Pipe()
161+
stdoutPipe = Pipe()
162+
stderrPipe = Pipe()
163+
closed = false
164+
queue = DispatchQueue(label: "com.intii.CopilotForXcode.IOTransport")
165+
166+
setupFileHandleHandlers()
167+
}
168+
169+
public func write(_ data: Data) {
170+
if closed {
171+
return
172+
}
173+
174+
let fileHandle = stdinPipe.fileHandleForWriting
175+
176+
queue.async {
177+
fileHandle.write(data)
178+
}
179+
}
180+
181+
public func close() {
182+
queue.sync {
183+
if self.closed {
184+
return
185+
}
186+
187+
self.closed = true
188+
189+
[stdoutPipe, stderrPipe, stdinPipe].forEach { pipe in
190+
pipe.fileHandleForWriting.closeFile()
191+
pipe.fileHandleForReading.closeFile()
192+
}
193+
}
194+
}
195+
196+
private func setupFileHandleHandlers() {
197+
stdoutPipe.fileHandleForReading.readabilityHandler = { [unowned self] handle in
198+
let data = handle.availableData
199+
200+
guard !data.isEmpty else {
201+
return
202+
}
203+
204+
#if DEBUG
205+
self.forwardDataToHandler(data)
206+
#endif
207+
}
208+
209+
stderrPipe.fileHandleForReading.readabilityHandler = { [unowned self] handle in
210+
let data = handle.availableData
211+
212+
guard !data.isEmpty else {
213+
return
214+
}
215+
216+
#if DEBUG
217+
self.forwardErrorDataToHandler(data)
218+
#endif
219+
}
220+
}
221+
222+
private func forwardDataToHandler(_ data: Data) {
223+
queue.async { [weak self] in
224+
guard let self = self else { return }
225+
226+
if self.closed {
227+
return
228+
}
229+
230+
if let string = String(bytes: data, encoding: .utf8) {
231+
Logger.codeium.info("stdout: \(string)")
232+
}
233+
}
234+
}
235+
236+
private func forwardErrorDataToHandler(_ data: Data) {
237+
queue.async {
238+
if let string = String(bytes: data, encoding: .utf8) {
239+
Logger.codeium.error("stderr: \(string)")
240+
}
241+
}
242+
}
243+
}
244+

0 commit comments

Comments
 (0)