diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 94affa6d..44d724ae 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -218,7 +218,7 @@ struct GitHubCopilotView: View { case let .installed(version): Text("Copilot.Vim Version: \(version)") uninstallButton - case let .outdated(version, latest): + case let .outdated(version, latest, _): Text("Copilot.Vim Version: \(version) (Update Available: \(latest))") updateButton uninstallButton diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift index 6e9ffab9..5cd6ba23 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift @@ -28,7 +28,7 @@ struct ToastPanelView: View { }() as Color, in: RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8) - .stroke(Color.black.opacity(0.3), lineWidth: 1) + .stroke(Color.black.opacity(0.1), lineWidth: 1) } } diff --git a/Tool/Package.swift b/Tool/Package.swift index 36ecdb15..a42040e0 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -316,6 +316,7 @@ let package = Package( "Preferences", "Terminal", "BuiltinExtension", + "Toast", .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), ], diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift index 0c3b8615..e893c133 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift @@ -1,14 +1,35 @@ import Foundation import Logger import Workspace +import Toast +import Dependencies public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin { + enum Error: Swift.Error, LocalizedError { + case gitHubCopilotLanguageServerMustBeUpdated + var errorDescription: String? { + switch self { + case .gitHubCopilotLanguageServerMustBeUpdated: + return "GitHub Copilot language server must be updated. Update will start immediately. \nIf it fails, please go to Host app > Service > GitHub Copilot and check if there is an update available." + } + } + } + + @Dependency(\.toast) var toast + + let installationManager = GitHubCopilotInstallationManager() private var _gitHubCopilotService: GitHubCopilotService? @GitHubCopilotSuggestionActor var gitHubCopilotService: GitHubCopilotService? { if let service = _gitHubCopilotService { return service } do { return try createGitHubCopilotService() + } catch let error as Error { + toast(error.localizedDescription, .warning) + Task { + await updateLanguageServerIfPossible() + } + return nil } catch { Logger.gitHubCopilot.error("Failed to create GitHub Copilot service: \(error)") return nil @@ -23,6 +44,9 @@ public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin { @GitHubCopilotSuggestionActor func createGitHubCopilotService() throws -> GitHubCopilotService { + if case .outdated(_, _, true) = installationManager.checkInstallation() { + throw Error.gitHubCopilotLanguageServerMustBeUpdated + } let newService = try GitHubCopilotService(projectRootURL: projectRootURL) _gitHubCopilotService = newService newService.localProcessServer?.terminationHandler = { [weak self] in @@ -50,6 +74,30 @@ public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin { } } } + + @GitHubCopilotSuggestionActor + func updateLanguageServerIfPossible() async { + guard !GitHubCopilotInstallationManager.isInstalling else { return } + let events = installationManager.installLatestVersion() + do { + for try await event in events { + switch event { + case .downloading: + toast("Updating GitHub Copilot language server", .info) + case .uninstalling: + break + case .decompressing: + break + case .done: + toast("Finished updating GitHub Copilot language server", .info) + } + } + } catch GitHubCopilotInstallationManager.Error.isInstalling { + return + } catch { + toast(error.localizedDescription, .error) + } + } func terminate() { _gitHubCopilotService = nil diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index efd8576b..94165428 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -2,7 +2,8 @@ import Foundation import Terminal public struct GitHubCopilotInstallationManager { - private static var isInstalling = false + @GitHubCopilotSuggestionActor + public private(set) static var isInstalling = false static var downloadURL: URL { let commitHash = "c79d711cbf7c6672c6c57d6df7c5ab7b6cac2b7a" @@ -11,13 +12,14 @@ public struct GitHubCopilotInstallationManager { } static let latestSupportedVersion = "1.33.0" + static let minimumSupportedVersion = "1.32.0" public init() {} public enum InstallationStatus { case notInstalled case installed(String) - case outdated(current: String, latest: String) + case outdated(current: String, latest: String, mandatory: Bool) case unsupported(current: String, latest: String) } @@ -38,7 +40,14 @@ public struct GitHubCopilotInstallationManager { { switch version.compare(Self.latestSupportedVersion) { case .orderedAscending: - return .outdated(current: version, latest: Self.latestSupportedVersion) + switch version.compare(Self.minimumSupportedVersion) { + case .orderedAscending: + return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: true) + case .orderedSame: + return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: false) + case .orderedDescending: + return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: false) + } case .orderedSame: return .installed(version) case .orderedDescending: @@ -46,7 +55,7 @@ public struct GitHubCopilotInstallationManager { } } - return .outdated(current: "Unknown", latest: Self.latestSupportedVersion) + return .outdated(current: "Unknown", latest: Self.latestSupportedVersion, mandatory: false) } public enum InstallationStep { @@ -75,7 +84,7 @@ public struct GitHubCopilotInstallationManager { public func installLatestVersion() -> AsyncThrowingStream { AsyncThrowingStream { continuation in - Task { + Task { @GitHubCopilotSuggestionActor in guard !GitHubCopilotInstallationManager.isInstalling else { continuation.finish(throwing: Error.isInstalling) return diff --git a/appcast.xml b/appcast.xml index 8cccd8f8..48f08812 100644 --- a/appcast.xml +++ b/appcast.xml @@ -9,7 +9,7 @@ 0.33.2 12.0 https://github.com/intitni/CopilotForXcode/releases/tag/0.33.2 - + 0.33.1