import Foundation import LanguageClient import LanguageServerProtocol import Logger import Preferences import SuggestionModel public protocol GitHubCopilotAuthServiceType { func checkStatus() async throws -> GitHubCopilotAccountStatus func signInInitiate() async throws -> (verificationUri: String, userCode: String) func signInConfirm(userCode: String) async throws -> (username: String, status: GitHubCopilotAccountStatus) func signOut() async throws -> GitHubCopilotAccountStatus func version() async throws -> String } public protocol GitHubCopilotSuggestionServiceType { func getCompletions( fileURL: URL, content: String, cursorPosition: CursorPosition, tabSize: Int, indentSize: Int, usesTabsForIndentation: Bool, ignoreSpaceOnlySuggestions: Bool, ignoreTrailingNewLinesAndSpaces: Bool ) async throws -> [CodeSuggestion] func notifyAccepted(_ completion: CodeSuggestion) async func notifyRejected(_ completions: [CodeSuggestion]) async func notifyOpenTextDocument(fileURL: URL, content: String) async throws func notifyChangeTextDocument(fileURL: URL, content: String) async throws func notifyCloseTextDocument(fileURL: URL) async throws func notifySaveTextDocument(fileURL: URL) async throws func cancelRequest() async func terminate() async } protocol GitHubCopilotLSP { func sendRequest(_ endpoint: E) async throws -> E.Response func sendNotification(_ notif: ClientNotification) async throws } enum GitHubCopilotError: Error, LocalizedError { case languageServerNotInstalled var errorDescription: String? { switch self { case .languageServerNotInstalled: return "Language server is not installed." } } } public class GitHubCopilotBaseService { let projectRootURL: URL var server: GitHubCopilotLSP var localProcessServer: CopilotLocalProcessServer? init(designatedServer: GitHubCopilotLSP) { projectRootURL = URL(fileURLWithPath: "/") server = designatedServer } init(projectRootURL: URL) throws { self.projectRootURL = projectRootURL let (server, localServer) = try { let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) let agentJSURL = urls.executableURL.appendingPathComponent("copilot/dist/agent.js") guard FileManager.default.fileExists(atPath: agentJSURL.path) else { throw GitHubCopilotError.languageServerNotInstalled } switch runner { case .bash: let nodePath = UserDefaults.shared.value(for: \.nodePath) let command = [ nodePath.isEmpty ? "node" : nodePath, "\"\(agentJSURL.path)\"", "--stdio", ].joined(separator: " ") executionParams = Process.ExecutionParameters( path: "/bin/bash", arguments: ["-i", "-l", "-c", command], environment: [:], currentDirectoryURL: urls.supportURL ) case .shell: let shell = ProcessInfo.processInfo.shellExecutablePath let nodePath = UserDefaults.shared.value(for: \.nodePath) let command = [ nodePath.isEmpty ? "node" : nodePath, "\"\(agentJSURL.path)\"", "--stdio", ].joined(separator: " ") executionParams = Process.ExecutionParameters( path: shell, arguments: ["-i", "-l", "-c", command], environment: [:], currentDirectoryURL: urls.supportURL ) case .env: let userEnvPath = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" executionParams = { let nodePath = UserDefaults.shared.value(for: \.nodePath) return Process.ExecutionParameters( path: "/usr/bin/env", arguments: [ nodePath.isEmpty ? "node" : nodePath, agentJSURL.path, "--stdio", ], environment: [ "PATH": userEnvPath, ], currentDirectoryURL: urls.supportURL ) }() } let localServer = CopilotLocalProcessServer(executionParameters: executionParams) localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) localServer.notificationHandler = { _, respond in respond(.timeout) } let server = InitializingServer(server: localServer) server.initializeParamsProvider = { let capabilities = ClientCapabilities( workspace: nil, textDocument: nil, window: nil, general: nil, experimental: nil ) return InitializeParams( processId: Int(ProcessInfo.processInfo.processIdentifier), clientInfo: .init( name: Bundle.main .object(forInfoDictionaryKey: "HOST_APP_NAME") as? String ?? "Copilot for Xcode" ), locale: nil, rootPath: projectRootURL.path, rootUri: projectRootURL.path, initializationOptions: nil, capabilities: capabilities, trace: .off, workspaceFolders: nil ) } return (server, localServer) }() self.server = server localProcessServer = localServer Task { try await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) } } public static func createFoldersIfNeeded() throws -> ( applicationSupportURL: URL, gitHubCopilotURL: URL, executableURL: URL, supportURL: URL ) { guard let supportURL = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask ).first?.appendingPathComponent( Bundle.main .object(forInfoDictionaryKey: "APPLICATION_SUPPORT_FOLDER") as! String ) else { throw CancellationError() } if !FileManager.default.fileExists(atPath: supportURL.path) { try? FileManager.default .createDirectory(at: supportURL, withIntermediateDirectories: false) } let gitHubCopilotFolderURL = supportURL.appendingPathComponent("GitHub Copilot") if !FileManager.default.fileExists(atPath: gitHubCopilotFolderURL.path) { try? FileManager.default .createDirectory(at: gitHubCopilotFolderURL, withIntermediateDirectories: false) } let supportFolderURL = gitHubCopilotFolderURL.appendingPathComponent("support") if !FileManager.default.fileExists(atPath: supportFolderURL.path) { try? FileManager.default .createDirectory(at: supportFolderURL, withIntermediateDirectories: false) } let executableFolderURL = gitHubCopilotFolderURL.appendingPathComponent("executable") if !FileManager.default.fileExists(atPath: executableFolderURL.path) { try? FileManager.default .createDirectory(at: executableFolderURL, withIntermediateDirectories: false) } return (supportURL, gitHubCopilotFolderURL, executableFolderURL, supportFolderURL) } } public final class GitHubCopilotAuthService: GitHubCopilotBaseService, GitHubCopilotAuthServiceType { public init() throws { let home = FileManager.default.homeDirectoryForCurrentUser try super.init(projectRootURL: home) } public func checkStatus() async throws -> GitHubCopilotAccountStatus { try await server.sendRequest(GitHubCopilotRequest.CheckStatus()).status } public func signInInitiate() async throws -> (verificationUri: String, userCode: String) { let result = try await server.sendRequest(GitHubCopilotRequest.SignInInitiate()) return (result.verificationUri, result.userCode) } public func signInConfirm(userCode: String) async throws -> (username: String, status: GitHubCopilotAccountStatus) { let result = try await server .sendRequest(GitHubCopilotRequest.SignInConfirm(userCode: userCode)) return (result.user, result.status) } public func signOut() async throws -> GitHubCopilotAccountStatus { try await server.sendRequest(GitHubCopilotRequest.SignOut()).status } public func version() async throws -> String { try await server.sendRequest(GitHubCopilotRequest.GetVersion()).version } } @globalActor public enum GitHubCopilotSuggestionActor { public actor TheActor {} public static let shared = TheActor() } @GitHubCopilotSuggestionActor public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, GitHubCopilotSuggestionServiceType { private var ongoingTasks = Set>() override public init(projectRootURL: URL = URL(fileURLWithPath: "/")) throws { try super.init(projectRootURL: projectRootURL) } override init(designatedServer: GitHubCopilotLSP) { super.init(designatedServer: designatedServer) } public func getCompletions( fileURL: URL, content: String, cursorPosition: CursorPosition, tabSize: Int, indentSize: Int, usesTabsForIndentation: Bool, ignoreSpaceOnlySuggestions: Bool, ignoreTrailingNewLinesAndSpaces: Bool ) async throws -> [CodeSuggestion] { let languageId = languageIdentifierFromFileURL(fileURL) let relativePath = { let filePath = fileURL.path let rootPath = projectRootURL.path if let range = filePath.range(of: rootPath), range.lowerBound == filePath.startIndex { let relativePath = filePath.replacingCharacters( in: filePath.startIndex..(_ endpoint: E) async throws -> E.Response { try await sendRequest(endpoint.request) } }