Skip to content

Commit cb901f9

Browse files
committed
Convert GitHub Copilot to a builtin extension
1 parent 8a42e7a commit cb901f9

9 files changed

Lines changed: 291 additions & 6 deletions
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import CopilotForXcodeKit
2+
import Foundation
3+
import Workspace
4+
import Logger
5+
import BuiltinExtension
6+
7+
public final class GitHubCopilotExtension: BuiltinExtension {
8+
public var suggestionService: SuggestionServiceType? { _suggestionService }
9+
public var chatService: ChatServiceType? { nil }
10+
public var promptToCodeService: PromptToCodeServiceType? { nil }
11+
let workspacePool: WorkspacePool
12+
13+
let serviceLocator: ServiceLocator
14+
let _suggestionService: GitHubCopilotSuggestionService
15+
16+
init(workspacePool: WorkspacePool) {
17+
self.workspacePool = workspacePool
18+
serviceLocator = .init(workspacePool: workspacePool)
19+
_suggestionService = .init(serviceLocator: serviceLocator)
20+
}
21+
22+
public func workspaceDidOpen(_: WorkspaceInfo) {}
23+
24+
public func workspaceDidClose(_: WorkspaceInfo) {}
25+
26+
public func workspace(_ workspace: WorkspaceInfo, didOpenDocumentAt documentURL: URL) {
27+
// check if file size is larger than 15MB, if so, return immediately
28+
if let attrs = try? FileManager.default
29+
.attributesOfItem(atPath: documentURL.path),
30+
let fileSize = attrs[FileAttributeKey.size] as? UInt64,
31+
fileSize > 15 * 1024 * 1024
32+
{ return }
33+
34+
Task {
35+
do {
36+
let content = try String(contentsOf: documentURL, encoding: .utf8)
37+
guard let service = await serviceLocator.getService(from: workspace) else { return }
38+
try await service.notifyOpenTextDocument(fileURL: documentURL, content: content)
39+
} catch {
40+
Logger.gitHubCopilot.error(error.localizedDescription)
41+
}
42+
}
43+
}
44+
45+
public func workspace(_ workspace: WorkspaceInfo, didSaveDocumentAt documentURL: URL) {
46+
Task {
47+
do {
48+
guard let service = await serviceLocator.getService(from: workspace) else { return }
49+
try await service.notifySaveTextDocument(fileURL: documentURL)
50+
} catch {
51+
Logger.gitHubCopilot.error(error.localizedDescription)
52+
}
53+
}
54+
}
55+
56+
public func workspace(_ workspace: WorkspaceInfo, didCloseDocumentAt documentURL: URL) {
57+
Task {
58+
do {
59+
guard let service = await serviceLocator.getService(from: workspace) else { return }
60+
try await service.notifyCloseTextDocument(fileURL: documentURL)
61+
} catch {
62+
Logger.gitHubCopilot.error(error.localizedDescription)
63+
}
64+
}
65+
}
66+
67+
public func workspace(
68+
_ workspace: WorkspaceInfo,
69+
didUpdateDocumentAt documentURL: URL,
70+
content: String
71+
) {
72+
// check if file size is larger than 15MB, if so, return immediately
73+
if let attrs = try? FileManager.default
74+
.attributesOfItem(atPath: documentURL.path),
75+
let fileSize = attrs[FileAttributeKey.size] as? UInt64,
76+
fileSize > 15 * 1024 * 1024
77+
{ return }
78+
79+
Task {
80+
do {
81+
let content = try String(contentsOf: documentURL, encoding: .utf8)
82+
guard let service = await serviceLocator.getService(from: workspace) else { return }
83+
try await service.notifyOpenTextDocument(fileURL: documentURL, content: content)
84+
} catch {
85+
Logger.gitHubCopilot.error(error.localizedDescription)
86+
}
87+
}
88+
}
89+
90+
public func appConfigurationDidChange(_ configuration: AppConfiguration) {
91+
if !configuration.chatServiceInUse && !configuration.suggestionServiceInUse {
92+
for workspace in workspacePool.workspaces.values {
93+
guard let plugin = workspace.plugin(for: GitHubCopilotWorkspacePlugin.self)
94+
else { continue }
95+
plugin.terminate()
96+
}
97+
}
98+
}
99+
100+
public func terminate() {
101+
for workspace in workspacePool.workspaces.values {
102+
guard let plugin = workspace.plugin(for: GitHubCopilotWorkspacePlugin.self)
103+
else { continue }
104+
plugin.terminate()
105+
}
106+
}
107+
}
108+
109+
final class ServiceLocator {
110+
let workspacePool: WorkspacePool
111+
112+
init(workspacePool: WorkspacePool) {
113+
self.workspacePool = workspacePool
114+
}
115+
116+
func getService(from workspace: WorkspaceInfo) async -> GitHubCopilotService? {
117+
guard let workspace = workspacePool.workspaces[workspace.workspaceURL],
118+
let plugin = workspace.plugin(for: GitHubCopilotWorkspacePlugin.self)
119+
else { return nil }
120+
return plugin.gitHubCopilotService
121+
}
122+
}
123+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Foundation
2+
import Workspace
3+
4+
public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin {
5+
var _gitHubCopilotService: GitHubCopilotService?
6+
var gitHubCopilotService: GitHubCopilotService? {
7+
if let service = _gitHubCopilotService { return service }
8+
do {
9+
return try createGitHubCopilotService()
10+
} catch {
11+
return nil
12+
}
13+
}
14+
15+
deinit {
16+
if let gitHubCopilotService {
17+
Task { await gitHubCopilotService.terminate() }
18+
}
19+
}
20+
21+
func createGitHubCopilotService() throws -> GitHubCopilotService {
22+
let newService = try GitHubCopilotService(projectRootURL: projectRootURL)
23+
_gitHubCopilotService = newService
24+
Task {
25+
try await Task.sleep(nanoseconds: 1_000_000_000)
26+
finishLaunchingService()
27+
}
28+
return newService
29+
}
30+
31+
func finishLaunchingService() {
32+
guard let workspace, let _gitHubCopilotService else { return }
33+
Task {
34+
for (_, filespace) in workspace.filespaces {
35+
let documentURL = filespace.fileURL
36+
guard let content = try? String(contentsOf: documentURL) else { continue }
37+
try? await _gitHubCopilotService.notifyOpenTextDocument(
38+
fileURL: documentURL,
39+
content: content
40+
)
41+
}
42+
}
43+
}
44+
45+
func terminate() {
46+
_gitHubCopilotService = nil
47+
}
48+
}
49+

Tool/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift

File renamed without changes.

Tool/Sources/GitHubCopilotService/CustomStdioTransport.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/CustomStdioTransport.swift

File renamed without changes.

Tool/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotAccountStatus.swift

File renamed without changes.

Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift

File renamed without changes.

Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift

File renamed without changes.

Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift renamed to Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,12 @@ public class GitHubCopilotBaseService {
196196
self.server = server
197197
localProcessServer = localServer
198198

199+
let notifications = NotificationCenter.default
200+
.notifications(named: .gitHubCopilotShouldRefreshEditorInformation)
199201
Task { [weak self] in
200202
_ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
201203

202-
for await _ in NotificationCenter.default
203-
.notifications(named: .gitHubCopilotShouldRefreshEditorInformation)
204-
{
205-
print("Yes!")
204+
for await _ in notifications {
206205
guard self != nil else { return }
207206
_ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
208207
}
@@ -318,8 +317,7 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService,
318317
public static let shared = TheActor()
319318
}
320319

321-
@GitHubCopilotSuggestionActor
322-
public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
320+
public final class GitHubCopilotService: GitHubCopilotBaseService,
323321
GitHubCopilotSuggestionServiceType
324322
{
325323
private var ongoingTasks = Set<Task<[CodeSuggestion], Error>>()
@@ -332,6 +330,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
332330
super.init(designatedServer: designatedServer)
333331
}
334332

333+
@GitHubCopilotSuggestionActor
335334
public func getCompletions(
336335
fileURL: URL,
337336
content: String,
@@ -393,22 +392,26 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
393392
return try await task.value
394393
}
395394

395+
@GitHubCopilotSuggestionActor
396396
public func cancelRequest() async {
397397
await localProcessServer?.cancelOngoingTasks()
398398
}
399399

400+
@GitHubCopilotSuggestionActor
400401
public func notifyAccepted(_ completion: CodeSuggestion) async {
401402
_ = try? await server.sendRequest(
402403
GitHubCopilotRequest.NotifyAccepted(completionUUID: completion.id)
403404
)
404405
}
405406

407+
@GitHubCopilotSuggestionActor
406408
public func notifyRejected(_ completions: [CodeSuggestion]) async {
407409
_ = try? await server.sendRequest(
408410
GitHubCopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.id))
409411
)
410412
}
411413

414+
@GitHubCopilotSuggestionActor
412415
public func notifyOpenTextDocument(
413416
fileURL: URL,
414417
content: String
@@ -430,6 +433,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
430433
)
431434
}
432435

436+
@GitHubCopilotSuggestionActor
433437
public func notifyChangeTextDocument(fileURL: URL, content: String) async throws {
434438
let uri = "file://\(fileURL.path)"
435439
// Logger.service.debug("Change \(uri), \(content.count)")
@@ -448,18 +452,21 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
448452
)
449453
}
450454

455+
@GitHubCopilotSuggestionActor
451456
public func notifySaveTextDocument(fileURL: URL) async throws {
452457
let uri = "file://\(fileURL.path)"
453458
// Logger.service.debug("Save \(uri)")
454459
try await server.sendNotification(.didSaveTextDocument(.init(uri: uri)))
455460
}
456461

462+
@GitHubCopilotSuggestionActor
457463
public func notifyCloseTextDocument(fileURL: URL) async throws {
458464
let uri = "file://\(fileURL.path)"
459465
// Logger.service.debug("Close \(uri)")
460466
try await server.sendNotification(.didCloseTextDocument(.init(uri: uri)))
461467
}
462468

469+
@GitHubCopilotSuggestionActor
463470
public func terminate() async {
464471
// automatically handled
465472
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import CopilotForXcodeKit
2+
import Foundation
3+
import SuggestionModel
4+
import Workspace
5+
6+
class GitHubCopilotSuggestionService: SuggestionServiceType {
7+
var configuration: SuggestionServiceConfiguration {
8+
.init(
9+
acceptsRelevantCodeSnippets: true,
10+
mixRelevantCodeSnippetsInSource: true,
11+
acceptsRelevantSnippetsFromOpenedFiles: false
12+
)
13+
}
14+
15+
let serviceLocator: ServiceLocator
16+
17+
init(serviceLocator: ServiceLocator) {
18+
self.serviceLocator = serviceLocator
19+
}
20+
21+
func getSuggestions(
22+
_ request: SuggestionRequest,
23+
workspace: WorkspaceInfo
24+
) async throws -> [CopilotForXcodeKit.CodeSuggestion] {
25+
guard let service = await serviceLocator.getService(from: workspace) else { return [] }
26+
return try await service.getCompletions(
27+
fileURL: request.fileURL,
28+
content: request.content,
29+
cursorPosition: .init(
30+
line: request.cursorPosition.line,
31+
character: request.cursorPosition.character
32+
),
33+
tabSize: request.tabSize,
34+
indentSize: request.indentSize,
35+
usesTabsForIndentation: request.usesTabsForIndentation
36+
).map(Self.convert)
37+
}
38+
39+
func notifyAccepted(
40+
_ suggestion: CopilotForXcodeKit.CodeSuggestion,
41+
workspace: WorkspaceInfo
42+
) async {
43+
guard let service = await serviceLocator.getService(from: workspace) else { return }
44+
await service.notifyAccepted(Self.convert(suggestion))
45+
}
46+
47+
func notifyRejected(
48+
_ suggestions: [CopilotForXcodeKit.CodeSuggestion],
49+
workspace: WorkspaceInfo
50+
) async {
51+
guard let service = await serviceLocator.getService(from: workspace) else { return }
52+
await service.notifyRejected(suggestions.map(Self.convert))
53+
}
54+
55+
func cancelRequest(workspace: WorkspaceInfo) async {
56+
guard let service = await serviceLocator.getService(from: workspace) else { return }
57+
await service.cancelRequest()
58+
}
59+
60+
static func convert(
61+
_ suggestion: SuggestionModel.CodeSuggestion
62+
) -> CopilotForXcodeKit.CodeSuggestion {
63+
.init(
64+
id: suggestion.id,
65+
text: suggestion.text,
66+
position: .init(
67+
line: suggestion.position.line,
68+
character: suggestion.position.character
69+
),
70+
range: .init(
71+
start: .init(
72+
line: suggestion.range.start.line,
73+
character: suggestion.range.start.character
74+
),
75+
end: .init(
76+
line: suggestion.range.end.line,
77+
character: suggestion.range.end.character
78+
)
79+
)
80+
)
81+
}
82+
83+
static func convert(
84+
_ suggestion: CopilotForXcodeKit.CodeSuggestion
85+
) -> SuggestionModel.CodeSuggestion {
86+
.init(
87+
id: suggestion.id,
88+
text: suggestion.text,
89+
position: .init(
90+
line: suggestion.position.line,
91+
character: suggestion.position.character
92+
),
93+
range: .init(
94+
start: .init(
95+
line: suggestion.range.start.line,
96+
character: suggestion.range.start.character
97+
),
98+
end: .init(
99+
line: suggestion.range.end.line,
100+
character: suggestion.range.end.character
101+
)
102+
)
103+
)
104+
}
105+
}
106+

0 commit comments

Comments
 (0)