From fa576974fe7b21c54a79560d7b04d08e5b56dc08 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 4 Oct 2024 19:41:08 +0000 Subject: [PATCH] Pre-release 0.24.78 --- Copilot for Xcode.xcodeproj/project.pbxproj | 8 + .../copilotIcon.imageset/Contents.json | 12 + .../CopilotforXcode-Icon@256w_1x.png | Bin 0 -> 42055 bytes .../AccountSettings/GitHubCopilotView.swift | 267 ---------- Core/Sources/HostApp/GeneralView.swift | 493 ++++++++++++------ .../HostApp/GitHubCopilotViewModel.swift | 132 +++++ Core/Sources/HostApp/InstructionSheet.swift | 82 +++ Core/Sources/HostApp/StringConstants.swift | 30 ++ Core/Sources/HostApp/TabContainer.swift | 34 +- .../TabToAcceptSuggestion.swift | 42 +- .../TabToAcceptSuggestionTests.swift | 71 ++- README.md | 15 +- 12 files changed, 703 insertions(+), 483 deletions(-) create mode 100644 Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/Contents.json create mode 100644 Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/CopilotforXcode-Icon@256w_1x.png delete mode 100644 Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift create mode 100644 Core/Sources/HostApp/GitHubCopilotViewModel.swift create mode 100644 Core/Sources/HostApp/InstructionSheet.swift create mode 100644 Core/Sources/HostApp/StringConstants.swift diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 7a1336f..20a790e 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 424ACA212CA4697200FA20F2 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 424ACA202CA4697200FA20F2 /* Credits.rtf */; }; 427C63282C6E868B000E557C /* OpenSettingsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427C63272C6E868B000E557C /* OpenSettingsCommand.swift */; }; 42888D512C66B10100DEF835 /* AuthStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42888D502C66B10100DEF835 /* AuthStatusChecker.swift */; }; + 5EC511E32C90CE7400632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; }; + 5EC511E42C90CE9800632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; }; + 5EC511E52C90CFD600632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; }; + 5EC511E62C90CFD700632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; }; C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */; }; C8009C032941C576007AA7E8 /* SyncTextSettingsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* SyncTextSettingsCommand.swift */; }; C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; }; @@ -651,6 +655,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5EC511E52C90CFD600632BAB /* Assets.xcassets in Resources */, + 5EC511E32C90CE7400632BAB /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -663,6 +669,7 @@ C8189B1E2938973000C9DCDA /* Assets.xcassets in Resources */, 3ABBEA292C8B9FE100C61D61 /* copilot-language-server in Resources */, 3ABBEA2B2C8BA00300C61D61 /* copilot-language-server-arm64 in Resources */, + 5EC511E62C90CFD700632BAB /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -674,6 +681,7 @@ C861E6152994F6080056CB02 /* Assets.xcassets in Resources */, 3ABBEA2D2C8BA00B00C61D61 /* copilot-language-server in Resources */, C81291D72994FE6900196E12 /* Main.storyboard in Resources */, + 5EC511E42C90CE9800632BAB /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/Contents.json new file mode 100644 index 0000000..ae075ee --- /dev/null +++ b/Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CopilotforXcode-Icon@256w_1x.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/CopilotforXcode-Icon@256w_1x.png b/Copilot for Xcode/Assets.xcassets/copilotIcon.imageset/CopilotforXcode-Icon@256w_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7674f663f683d3c8b14012770ac9d7e37ac9e4d8 GIT binary patch literal 42055 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

_S? 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 GitHubCopilotAuthServiceType { - if let service = Self.copilotAuthService { return service } - let service = try GitHubCopilotService() - Self.copilotAuthService = service - return service - } - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("Language Server Version: \(version ?? "Loading..")") - .alert(isPresented: $xcodeBetaAccessAlert) { - Alert( - title: Text("Xcode Beta Access Not Granted"), - message: Text( - "Logged in user does not have access to GitHub Copilot for Xcode" - ), - dismissButton: .default(Text("Close")) - ) - } - - if waitingForSignIn { - Text("Status: Waiting for GitHub authentication") - } else { - Text(""" - Status: \(status?.description ?? "Loading..")\ - \(xcodeBetaAccessAlert ? " - Xcode Beta Access Not Granted" : "") - """) - } - - HStack(alignment: .center) { - Button("Refresh") { - checkStatus() - } - if waitingForSignIn { - Button("Cancel") { cancelWaiting() } - } else if status == .notSignedIn { - Button("Sign In") { signIn() } - .alert( - signInResponse?.userCode ?? "", - isPresented: $isSignInAlertPresented, - presenting: signInResponse) { _ in - Button("Cancel", role: .cancel, action: {}) - Button("Copy Code and Open", action: copyAndOpen) - } message: { response in - Text(""" - Please enter the above code in the \ - GitHub website to authorize your \ - GitHub account with Copilot for Xcode. - - \(response?.verificationURL.absoluteString ?? "") - """) - } - } - if status == .ok || status == .alreadySignedIn || - status == .notAuthorized - { - Button("Sign Out") { signOut() } - } - if isRunningAction || waitingForSignIn { - ActivityIndicatorView() - } - } - .opacity(isRunningAction ? 0.8 : 1) - .disabled(isRunningAction) - } - .padding() - - Spacer() - } - .onAppear { - if isPreview { return } - if settings.disableGitHubCopilotSettingsAutoRefreshOnAppear { return } - checkStatus() - } - .textFieldStyle(.roundedBorder) - .onReceive(FeatureFlagNotifierImpl.shared.featureFlagsDidChange) { flags in - self.xcodeBetaAccessAlert = flags.x != true - } - } - - func checkStatus() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - let service = try getGitHubCopilotAuthService() - status = try await service.checkStatus() - version = try await service.version() - isRunningAction = false - - if status != .ok, status != .notSignedIn { - toast( - "GitHub Copilot status is not \"ok\". Please check if you have a valid GitHub Copilot subscription.", - - .error - ) - } - } catch { - toast(error.localizedDescription, .error) - } - } - } - - func signIn() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - let service = try getGitHubCopilotAuthService() - let (uri, userCode) = try await service.signInInitiate() - guard let url = URL(string: uri) else { - toast("Verification URI is incorrect.", .error) - return - } - self.signInResponse = .init(userCode: userCode, verificationURL: url) - isSignInAlertPresented = true - } catch { - toast(error.localizedDescription, .error) - } - } - } - - func copyAndOpen() { - waitingForSignIn = true - guard let signInResponse else { - toast("Missing sign in details.", .error) - return - } - // Copy the device code to the clipboard - let pasteboard = NSPasteboard.general - pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) - pasteboard.setString(signInResponse.userCode, forType: NSPasteboard.PasteboardType.string) - toast("Sign-in code \(signInResponse.userCode) copied", .info) - // Open verification URL in default browser - openURL(signInResponse.verificationURL) - // Wait for signInConfirm response - waitForSignIn() - } - - func waitForSignIn() { - Task { - do { - guard waitingForSignIn else { return } - guard let signInResponse else { - waitingForSignIn = false - return - } - let service = try getGitHubCopilotAuthService() - let (username, status) = try await service.signInConfirm(userCode: signInResponse.userCode) - waitingForSignIn = false - self.settings.username = username - self.status = status - } catch let error as GitHubCopilotError { - if case .languageServerError(.timeout) = error { - // TODO figure out how to extend the default timeout on an LSP request - // Until then, reissue request - waitForSignIn() - return - } - throw error - } catch { - toast(error.localizedDescription, .error) - } - } - - } - - func cancelWaiting() { - waitingForSignIn = false - } - - func signOut() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - let service = try getGitHubCopilotAuthService() - status = try await service.signOut() - } catch { - toast(error.localizedDescription, .error) - } - } - } - - func refreshConfiguration() { - NotificationCenter.default.post( - name: .gitHubCopilotShouldRefreshEditorInformation, - object: nil - ) - - Task { - let service = try getService() - do { - try await service.postNotification( - name: Notification.Name - .gitHubCopilotShouldRefreshEditorInformation.rawValue - ) - } catch { - toast(error.localizedDescription, .error) - } - } - } -} - -struct ActivityIndicatorView: NSViewRepresentable { - func makeNSView(context _: Context) -> NSProgressIndicator { - let progressIndicator = NSProgressIndicator() - progressIndicator.style = .spinning - progressIndicator.controlSize = .small - progressIndicator.startAnimation(nil) - return progressIndicator - } - - func updateNSView(_: NSProgressIndicator, context _: Context) { - // No-op - } -} - -struct CopilotView_Previews: PreviewProvider { - static var previews: some View { - VStack(alignment: .leading, spacing: 8) { - GitHubCopilotView(status: .notSignedIn, version: "1.0.0") - GitHubCopilotView(status: .alreadySignedIn, isRunningAction: true) - GitHubCopilotView(settings: .init(), status: .alreadySignedIn, xcodeBetaAccessAlert: true) - GitHubCopilotView(settings: .init(), status: .notSignedIn, waitingForSignIn: true) - } - .padding(.all, 8) - .previewLayout(.sizeThatFits) - } -} - diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index d865c15..4fd3d55 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -1,240 +1,395 @@ import Client +import GitHubCopilotService import ComposableArchitecture import KeyboardShortcuts import LaunchAgentManager import Preferences import SharedUIComponents import SwiftUI +import XPCShared +import Cocoa + +struct SignInResponse { + let userCode: String + let verificationURL: URL +} struct GeneralView: View { let store: StoreOf - + @StateObject private var viewModel = GitHubCopilotViewModel() + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { - AppInfoView(store: store) - SettingsDivider() - GitHubCopilotView() - SettingsDivider() - ExtensionServiceView(store: store) - SettingsDivider() - LaunchAgentView() - SettingsDivider() - GeneralSettingsView() + AppInfoView(viewModel: viewModel, store: store) + GeneralSettingsView(store: store) + CopilotConnectionView(viewModel: viewModel, store: store) + .padding(.bottom, 20) + Divider() + Spacer().frame(height: 40) + rightsView + .padding(.horizontal, 20) } + .frame(maxWidth: .infinity) } .onAppear { store.send(.appear) } } + + var rightsView: some View { + Text(StringConstants.rightsReserved) + .font(.caption2) + .foregroundColor(.secondary.opacity(0.5)) + } } struct AppInfoView: View { - @State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + class Settings: ObservableObject { + @AppStorage(\.installPrereleases) + var installPrereleases + } + + static var copilotAuthService: GitHubCopilotAuthServiceType? + @Environment(\.updateChecker) var updateChecker + @Environment(\.toast) var toast + + @StateObject var settings = Settings() + @StateObject var viewModel: GitHubCopilotViewModel + + @State var automaticallyCheckForUpdate: Bool? + @State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + let store: StoreOf - + var body: some View { VStack(alignment: .leading) { - HStack(alignment: .top) { - Text( - Bundle.main - .object(forInfoDictionaryKey: "HOST_APP_NAME") as? String - ?? "GitHub Copilot for Xcode" - ) - .font(.title) - Text(appVersion ?? "") - .font(.footnote) - .foregroundColor(.secondary) - - Spacer() - - Button(action: { - updateChecker.checkForUpdates() - }) { - HStack(spacing: 2) { - Image(systemName: "arrow.up.right.circle.fill") - Text("Check for Updates") - } - } - } - }.padding() - } -} - -struct ExtensionServiceView: View { - @Perception.Bindable var store: StoreOf - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading) { - Text("Extension Service Version: \(store.xpcServiceVersion ?? "Loading..")") - - let grantedStatus: String = { - guard let granted = store.isAccessibilityPermissionGranted - else { return "Loading.." } - return granted ? "Granted" : "Not Granted" - }() - Text("Accessibility Permission: \(grantedStatus)") - - HStack { - Button(action: { store.send(.reloadStatus) }) { - Text("Refresh") - }.disabled(store.isReloading) - - Button(action: { - Task { - let workspace = NSWorkspace.shared - let url = Bundle.main.bundleURL - .appendingPathComponent("Contents") - .appendingPathComponent("Applications") - .appendingPathComponent("GitHub Copilot for Xcode Extension.app") - workspace.activateFileViewerSelecting([url]) - } - }) { - Text("Reveal Extension Service in Finder") + HStack(alignment: .center, spacing: 16) { + Image("copilotIcon") + .resizable() + .frame(width: 110, height: 110) + VStack(alignment: .leading) { + HStack { + Text(Bundle.main.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String ?? StringConstants.appName) + .font(.title) + Text("(\(appVersion ?? ""))") + .font(.title) } - + Text("\(StringConstants.languageServerVersion) \(viewModel.version ?? StringConstants.loading)") Button(action: { - let url = URL( - string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - )! - NSWorkspace.shared.open(url) + updateChecker.checkForUpdates() }) { - Text("Accessibility Settings") + HStack(spacing: 2) { + Text(StringConstants.checkForUpdates) + } } - - Button(action: { - let url = URL( - string: "x-apple.systempreferences:com.apple.ExtensionsPreferences" - )! - NSWorkspace.shared.open(url) - }) { - Text("Extensions Settings") + HStack { + Toggle(isOn: .init( + get: { automaticallyCheckForUpdate ?? updateChecker.automaticallyChecksForUpdates }, + set: { + updateChecker.automaticallyChecksForUpdates = $0 + automaticallyCheckForUpdate = $0 + } + )) { + Text(StringConstants.automaticallyCheckForUpdates) + } + + Toggle(isOn: $settings.installPrereleases) { + Text(StringConstants.installPreReleases) + } } } + Spacer() } + .padding(.horizontal, 2) } .padding() + .onAppear { + if isPreview { return } + viewModel.checkStatus() + } } } -struct LaunchAgentView: View { - @Environment(\.toast) var toast - @State var isDidRemoveLaunchAgentAlertPresented = false - @State var isDidSetupLaunchAgentAlertPresented = false - @State var isDidRestartLaunchAgentAlertPresented = false +struct GeneralSettingsView: View { + + class Settings: ObservableObject { + @AppStorage(\.quitXPCServiceOnXcodeAndAppQuit) + var quitXPCServiceOnXcodeAndAppQuit + } + + @StateObject var settings = Settings() + @Environment(\.updateChecker) var updateChecker + @AppStorage(\.realtimeSuggestionToggle) var isCopilotEnabled: Bool + @State private var shouldPresentInstructionSheet = false + @State private var shouldPresentTurnoffSheet = false + + let store: StoreOf + var body: some View { VStack(alignment: .leading) { - HStack { - Button(action: { - Task { - do { - try await LaunchAgentManager().setupLaunchAgent() - isDidSetupLaunchAgentAlertPresented = true - } catch { - toast(error.localizedDescription, .error) - } + Text(StringConstants.general) + .bold() + .padding(.leading, 8) + VStack(spacing: .zero) { + HStack(alignment: .center) { + Text(StringConstants.quitCopilot) + .padding(.horizontal, 8) + Spacer() + Toggle(isOn: $settings.quitXPCServiceOnXcodeAndAppQuit) { } - }) { - Text("Set Up Launch Agent") - } - .alert(isPresented: $isDidSetupLaunchAgentAlertPresented) { - .init( - title: Text("Finished Launch Agent Setup"), - message: Text( - "Please refresh the Copilot status. (The first refresh may fail)" - ), - dismissButton: .default(Text("OK")) - ) + .toggleStyle(.switch) + .padding(.horizontal, 8) } - - Button(action: { - Task { - do { - try await LaunchAgentManager().removeLaunchAgent() - isDidRemoveLaunchAgentAlertPresented = true - } catch { - toast(error.localizedDescription, .error) + .padding(.vertical, 8) + + Divider() + Link(destination: URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) { + HStack { + VStack(alignment: .leading) { + let grantedStatus: String = { + guard let granted = store.isAccessibilityPermissionGranted else { return StringConstants.loading } + return granted ? "Granted" : "Not Granted" + }() + Text(StringConstants.accessibilityPermissions) + .font(.body) + Text("\(StringConstants.status) \(grantedStatus) ⓘ") + .font(.footnote) } + Spacer() + + Image(systemName: "control") + .rotationEffect(.degrees(90)) } - }) { - Text("Remove Launch Agent") } - .alert(isPresented: $isDidRemoveLaunchAgentAlertPresented) { - .init( - title: Text("Launch Agent Removed"), - dismissButton: .default(Text("OK")) - ) + .foregroundStyle(.primary) + .padding(8) + + Divider() + HStack { + VStack(alignment: .leading) { + let grantedStatus: String = { + guard let granted = store.isAccessibilityPermissionGranted else { return StringConstants.loading } + return granted ? "Granted" : "Not Granted" + }() + Text(StringConstants.extensionPermissions) + .font(.body) + Text("\(StringConstants.status) \(grantedStatus) ⓘ") + .font(.footnote) + .onTapGesture { + shouldPresentInstructionSheet = true + } + } + Spacer() + Link(destination: URL(string: "x-apple.systempreferences:com.apple.ExtensionsPreferences")!) { + Image(systemName: "control") + .rotationEffect(.degrees(90)) + } } - + .foregroundStyle(.primary) + .padding(.horizontal, 8) + .padding(.vertical, 8) + } + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + + HStack(alignment: .center) { + Spacer() Button(action: { - Task { - do { - try await LaunchAgentManager().reloadLaunchAgent() - isDidRestartLaunchAgentAlertPresented = true - } catch { - toast(error.localizedDescription, .error) - } + if isCopilotEnabled { + shouldPresentTurnoffSheet = true + } else { + isCopilotEnabled = true } }) { - Text("Reload Launch Agent") - }.alert(isPresented: $isDidRestartLaunchAgentAlertPresented) { - .init( - title: Text("Launch Agent Reloaded"), - dismissButton: .default(Text("OK")) - ) + Text(isCopilotEnabled ? StringConstants.turnOffCopilot : StringConstants.turnOnCopilot) + .padding(.horizontal, 8) } } } - .padding() + .padding(.horizontal, 20) + .sheet(isPresented: $shouldPresentInstructionSheet) { + } content: { + InstructionSheet { + shouldPresentInstructionSheet = false + } + } + .alert(isPresented: $shouldPresentTurnoffSheet) { + Alert( + title: Text(StringConstants.turnOffAlertTitle), + message: Text(StringConstants.turnOffAlertMessage), + primaryButton: .default(Text("Turn off").foregroundColor(.blue)){ + isCopilotEnabled = false + shouldPresentTurnoffSheet = false + }, + secondaryButton: .cancel(Text(StringConstants.cancel)) { + shouldPresentTurnoffSheet = false + } + ) + } } } -struct GeneralSettingsView: View { - class Settings: ObservableObject { - @AppStorage(\.quitXPCServiceOnXcodeAndAppQuit) - var quitXPCServiceOnXcodeAndAppQuit - @AppStorage(\.suggestionWidgetPositionMode) - var suggestionWidgetPositionMode - @AppStorage(\.widgetColorScheme) - var widgetColorScheme - @AppStorage(\.preferWidgetToStayInsideEditorWhenWidthGreaterThan) - var preferWidgetToStayInsideEditorWhenWidthGreaterThan - @AppStorage(\.showHideWidgetShortcutGlobally) - var showHideWidgetShortcutGlobally - @AppStorage(\.installPrereleases) - var installPrereleases - } - @StateObject var settings = Settings() - @Environment(\.updateChecker) var updateChecker - @State var automaticallyCheckForUpdate: Bool? +struct CopilotConnectionView: View { + @AppStorage("username") var username: String = "" + @Environment(\.toast) var toast + @StateObject var viewModel = GitHubCopilotViewModel() + @State var waitingForSignIn = false + let store: StoreOf + var body: some View { - Form { - Toggle(isOn: $settings.quitXPCServiceOnXcodeAndAppQuit) { - Text("Quit service when Xcode and host app are terminated") + WithPerceptionTracking { + VStack { + connection + .padding(.bottom, 20) + copilotResources } + } + } - Toggle(isOn: .init( - get: { automaticallyCheckForUpdate ?? updateChecker.automaticallyChecksForUpdates }, - set: { - updateChecker.automaticallyChecksForUpdates = $0 - automaticallyCheckForUpdate = $0 + var connection: some View { + VStack(alignment: .leading) { + Text(StringConstants.copilotResources) + .bold() + .padding(.leading, 8) + VStack(spacing: .zero) { + HStack(alignment: .center) { + VStack(alignment: .leading) { + Text(StringConstants.githubAccountStatus) + .font(.body) + Text("\(StringConstants.githubConnection) \(viewModel.status?.description ?? StringConstants.loading)") + .font(.footnote) + } + Spacer() + Button(StringConstants.refreshConnection) { + viewModel.checkStatus() + } + if waitingForSignIn { + Button(StringConstants.cancel) { + viewModel.cancelWaiting() + } + } else if viewModel.status == .notSignedIn { + Button(StringConstants.loginToGitHub) { + viewModel.signIn() + } + .alert( + viewModel.signInResponse?.userCode ?? "", + isPresented: $viewModel.isSignInAlertPresented, + presenting: viewModel.signInResponse) { _ in + Button(StringConstants.cancel, role: .cancel, action: {}) + Button("Copy Code and Open", action: viewModel.copyAndOpen) + } message: { response in + Text(""" + Please enter the above code in the \ + GitHub website to authorize your \ + GitHub account with Copilot for Xcode. + + \(response?.verificationURL.absoluteString ?? "") + """) + } + } + if viewModel.status == .ok || viewModel.status == .alreadySignedIn || + viewModel.status == .notAuthorized + { + Button(StringConstants.logoutFromGitHub) { viewModel.signOut() + viewModel.isSignInAlertPresented = false + } + } + if viewModel.isRunningAction || waitingForSignIn { + ActivityIndicatorView() + } } - )) { - Text("Automatically Check for Updates") + .opacity(viewModel.isRunningAction ? 0.8 : 1) + .disabled(viewModel.isRunningAction) + .padding(8) + + Divider() + Link(destination: URL(string: "https://github.com/settings/copilot")!) { + HStack { + Text(StringConstants.githubCopilotSettings) + .font(.body) + Spacer() + + Image(systemName: "control") + .rotationEffect(.degrees(90)) + } + } + .foregroundStyle(.primary) + .padding(.horizontal, 8) + .padding(.vertical, 10) } + .background(Color.gray.opacity(0.1)) + .cornerRadius(6) + } + .padding(.horizontal, 20) + .onAppear { + store.send(.reloadStatus) + viewModel.checkStatus() + } + } + + var copilotResources: some View { + VStack(alignment: .leading) { + Text(StringConstants.copilotResources) + .bold() + .padding(.leading, 8) + + VStack(spacing: .zero) { + let docURL = URL(string: (Bundle.main.object(forInfoDictionaryKey: "COPILOT_DOCS_URL") as? String) ?? "https://docs.github.com/en/copilot")! + Link(destination: docURL) { + HStack { + Text(StringConstants.copilotDocumentation) + .font(.body) + Spacer() + Image(systemName: "control") + .rotationEffect(.degrees(90)) + } + } + .foregroundStyle(.primary) + .padding(.horizontal, 8) + .padding(.vertical, 10) + + Divider() - Toggle(isOn: $settings.installPrereleases) { - Text("Install pre-releases") + let forumURL = URL(string: (Bundle.main.object(forInfoDictionaryKey: "COPILOT_FORUM_URL") as? String) ?? "https://github.com/orgs/community/discussions/categories/copilot")! + Link(destination: forumURL) { + HStack { + Text(StringConstants.copilotFeedbackForum) + .font(.body) + Spacer() + Image(systemName: "control") + .rotationEffect(.degrees(90)) + } + } + .foregroundStyle(.primary) + .padding(.horizontal, 8) + .padding(.vertical, 10) } - }.padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(6) + } + .padding(.horizontal, 20) } } +struct ActivityIndicatorView: NSViewRepresentable { + func makeNSView(context _: Context) -> NSProgressIndicator { + let progressIndicator = NSProgressIndicator() + progressIndicator.style = .spinning + progressIndicator.controlSize = .small + progressIndicator.startAnimation(nil) + return progressIndicator + } + + func updateNSView(_: NSProgressIndicator, context _: Context) { + // No-op + } +} + struct GeneralView_Previews: PreviewProvider { static var previews: some View { GeneralView(store: .init(initialState: .init(), reducer: { General() })) diff --git a/Core/Sources/HostApp/GitHubCopilotViewModel.swift b/Core/Sources/HostApp/GitHubCopilotViewModel.swift new file mode 100644 index 0000000..a06bad3 --- /dev/null +++ b/Core/Sources/HostApp/GitHubCopilotViewModel.swift @@ -0,0 +1,132 @@ +import GitHubCopilotService +import ComposableArchitecture +import KeyboardShortcuts +import LaunchAgentManager +import SwiftUI + + +@MainActor +class GitHubCopilotViewModel: ObservableObject { + @Dependency(\.toast) var toast + @Dependency(\.openURL) var openURL + + @AppStorage("username") var username: String = "" + + @Published var isRunningAction: Bool = false + @Published var status: GitHubCopilotAccountStatus? + @Published var version: String? + @Published var userCode: String? + @Published var isSignInAlertPresented = false + @Published var signInResponse: SignInResponse? + @Published var waitingForSignIn = false + + static var copilotAuthService: GitHubCopilotAuthServiceType? + + func getGitHubCopilotAuthService() throws -> GitHubCopilotAuthServiceType { + if let service = Self.copilotAuthService { return service } + let service = try GitHubCopilotService() + Self.copilotAuthService = service + return service + } + + func signIn() { + Task { + isRunningAction = true + defer { isRunningAction = false } + do { + let service = try getGitHubCopilotAuthService() + let (uri, userCode) = try await service.signInInitiate() + guard let url = URL(string: uri) else { + toast("Verification URI is incorrect.", .error) + return + } + self.signInResponse = .init(userCode: userCode, verificationURL: url) + self.isSignInAlertPresented = true + } catch { + toast(error.localizedDescription, .error) + } + } + } + + func checkStatus() { + Task { + isRunningAction = true + defer { isRunningAction = false } + do { + let service = try getGitHubCopilotAuthService() + status = try await service.checkStatus() + version = try await service.version() + isRunningAction = false + + if status != .ok, status != .notSignedIn { + toast( + "GitHub Copilot status is not \"ok\". Please check if you have a valid GitHub Copilot subscription.", + .error + ) + } + } catch { + toast(error.localizedDescription, .error) + } + } + } + + func signOut() { + Task { + isRunningAction = true + defer { isRunningAction = false } + do { + let service = try getGitHubCopilotAuthService() + status = try await service.signOut() + } catch { + toast(error.localizedDescription, .error) + } + } + } + + func cancelWaiting() { + waitingForSignIn = false + } + + func copyAndOpen() { + waitingForSignIn = true + guard let signInResponse else { + toast("Missing sign in details.", .error) + return + } + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) + pasteboard.setString(signInResponse.userCode, forType: NSPasteboard.PasteboardType.string) + toast("Sign-in code \(signInResponse.userCode) copied", .info) + Task { + await openURL(signInResponse.verificationURL) + waitForSignIn() + } + } + + func waitForSignIn() { + Task { + do { + guard waitingForSignIn else { return } + guard let signInResponse else { + waitingForSignIn = false + return + } + let service = try getGitHubCopilotAuthService() + let (username, status) = try await service.signInConfirm(userCode: signInResponse.userCode) + waitingForSignIn = false + self.username = username + self.status = status + } catch let error as GitHubCopilotError { + if case .languageServerError(.timeout) = error { + // TODO figure out how to extend the default timeout on a Chime LSP request + // Until then, reissue request + waitForSignIn() + return + } + throw error + } catch { + toast(error.localizedDescription, .error) + } + } + } +} diff --git a/Core/Sources/HostApp/InstructionSheet.swift b/Core/Sources/HostApp/InstructionSheet.swift new file mode 100644 index 0000000..c6e6d9a --- /dev/null +++ b/Core/Sources/HostApp/InstructionSheet.swift @@ -0,0 +1,82 @@ +import Foundation +import SwiftUI + +struct InstructionSheet: View { + let closeAction: () -> () + + var body: some View { + VStack(alignment: .center) { + Image("copilotIcon") + .resizable() + .frame(width: 64, height: 64, alignment: .center) + .padding(.top, 36) + .padding(.bottom, 16) + Text("Extension Permissions") + .fontWeight(.heavy) + .font(.system(size: 16)) + Text("To enable permissions in settings:") + .font(.system(size: 14)) + .padding(.top, 4) + + VStack(alignment: .center) { + HStack { + ZStack { + RoundedRectangle(cornerRadius: 3) + .fill(Color.blue) + .frame(width: 13, height: 13) + + Image(systemName: "checkmark") + .foregroundColor(.white) + .font(.system(size: 8, weight: .bold)) + } + + Text("Xcode Source Editor") + .font(.system(size: 12)) + + Image(systemName: "arrowshape.left.fill") + .resizable() + .foregroundColor(Color.red) + .frame(width: 40, height: 10) + } + .frame(height: 25) + .padding(.horizontal, 12) + + HStack { + Image("copilotIcon") + .resizable() + .frame(width: 15, height: 15, alignment: .center) + Text("GitHub Copilot for Xcode") + .font(.system(size: 12)) + } + .frame(height: 25) + .padding(.horizontal, 8) + } + .padding(.vertical, 4) + .background(Color.blue.opacity(0.1)) + .cornerRadius(5) + .padding(.vertical, 10) + + VStack(spacing: 0) { + Text("To view Copilot preferences in XCode, path:") + .font(.system(size: 12)) + .padding(.top, 16) + Text("Xcode Source Editor > GitHub Copilot") + .bold() + .font(.system(size: 12)) + } + .padding(.horizontal) + + Button(action: closeAction, label:{ + Text("Close") + .foregroundColor(.white) + .frame(height: 28) + .frame(maxWidth: .infinity) + }) + .buttonStyle(.borderedProminent) + .cornerRadius(5) + .padding(16) + .padding(.bottom, 16) + } + .frame(width: 300, height: 376) + } +} diff --git a/Core/Sources/HostApp/StringConstants.swift b/Core/Sources/HostApp/StringConstants.swift new file mode 100644 index 0000000..be7976c --- /dev/null +++ b/Core/Sources/HostApp/StringConstants.swift @@ -0,0 +1,30 @@ +struct StringConstants { + static let rightsReserved = "GitHub. All rights reserved." + static let appName = "GitHub Copilot for Xcode" + static let languageServerVersion = "Language Server Version:" + static let checkForUpdates = "Check for Updates" + static let automaticallyCheckForUpdates = "Automatically Check for Updates" + static let installPreReleases = "Install pre-releases" + static let general = "General" + static let quitCopilot = "Quit GitHub Copilot when Xcode App is closed" + static let accessibilityPermissions = "Accessibility Permissions" + static let extensionPermissions = "Extension Permissions" + static let status = "Status:" + static let cancel = "Cancel" + static let turnOff = "Turn off" + static let turnOffCopilot = "Turn off Copilot for Xcode" + static let turnOnCopilot = "Turn on Copilot for Xcode" + static let turnOffAlertTitle = "Turn off Copilot for Xcode" + static let turnOffAlertMessage = "If you turn off Copilot for Xcode, all features will be disabled." + static let githubAccountStatus = "Github Account Status Permissions" + static let githubConnection = "Github Connection:" + static let refreshConnection = "Refresh Connection" + static let loginToGitHub = "Login to GitHub" + static let confirmSignIn = "Confirm Sign-in" + static let logoutFromGitHub = "Logout from GitHub" + static let githubCopilotSettings = "GitHub Copilot Account Settings" + static let copilotResources = "Copilot Resources" + static let copilotDocumentation = "View Copilot Documentation" + static let copilotFeedbackForum = "View Copilot Feedback Forum" + static let loading = "Loading.." +} diff --git a/Core/Sources/HostApp/TabContainer.swift b/Core/Sources/HostApp/TabContainer.swift index 5602e86..e40db63 100644 --- a/Core/Sources/HostApp/TabContainer.swift +++ b/Core/Sources/HostApp/TabContainer.swift @@ -30,20 +30,18 @@ public struct TabContainer: View { VStack(spacing: 0) { TabBar(tag: $tag, tabBarItems: tabBarItems) .padding(.bottom, 8) - - Divider() - ZStack(alignment: .center) { GeneralView(store: store.scope(state: \.general, action: \.general)) .tabBarItem( tag: 0, title: "General", - image: "gearshape" + image: "CopilotLogo", + isSystemImage: false ) FeatureSettingsView().tabBarItem( tag: 2, title: "Feature", - image: "star.square" + image: "star.circle" ) } .environment(\.tabBarTabTag, tag) @@ -75,7 +73,8 @@ struct TabBar: View { currentTag: $tag, tag: tab.tag, title: tab.title, - image: tab.image + image: tab.image, + isSystemImage: tab.isSystemImage ) } } @@ -88,18 +87,29 @@ struct TabBarButton: View { var tag: Int var title: String var image: String + var isSystemImage: Bool = true + + private var tabImage: Image { + isSystemImage ? Image(systemName: image) : Image(image) + } + + private var isSelected: Bool { + tag == currentTag + } var body: some View { Button(action: { self.currentTag = tag }) { VStack(spacing: 2) { - Image(systemName: image) + tabImage + .renderingMode(.template) .resizable() .scaledToFit() - .frame(height: 18) + .frame(width: 24, height: 24) Text(title) } + .foregroundColor(isSelected ? .blue : .gray) .font(.body) .padding(.horizontal, 12) .padding(.vertical, 4) @@ -129,6 +139,7 @@ private struct TabBarTabViewWrapper: View { var tag: Int var title: String var image: String + var isSystemImage: Bool = true var content: () -> Content var body: some View { @@ -141,7 +152,7 @@ private struct TabBarTabViewWrapper: View { } .preference( key: TabBarItemPreferenceKey.self, - value: [.init(tag: tag, title: title, image: image)] + value: [.init(tag: tag, title: title, image: image, isSystemImage: isSystemImage)] ) } } @@ -150,12 +161,14 @@ private extension View { func tabBarItem( tag: Int, title: String, - image: String + image: String, + isSystemImage: Bool = true ) -> some View { TabBarTabViewWrapper( tag: tag, title: title, image: image, + isSystemImage: isSystemImage, content: { self } ) } @@ -166,6 +179,7 @@ private struct TabBarItem: Identifiable, Equatable { var tag: Int var title: String var image: String + var isSystemImage: Bool = true } private struct TabBarItemPreferenceKey: PreferenceKey { diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift index dbbacf9..f2d4c14 100644 --- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift +++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift @@ -121,11 +121,15 @@ final class TabToAcceptSuggestion { } func handleEvent(_ event: CGEvent) -> CGEventManipulation.Result { - if Self.shouldAcceptSuggestion( + let (accept, reason) = Self.shouldAcceptSuggestion( event: event, workspacePool: workspacePool, xcodeInspector: ThreadSafeAccessToXcodeInspector.shared - ) { + ) + if let reason = reason { + Logger.service.debug("TabToAcceptSuggestion: \(accept ? "" : "not") accepting due to: \(reason)") + } + if accept { acceptSuggestion() return .discarded } @@ -148,20 +152,30 @@ extension TabToAcceptSuggestion { event: CGEvent, workspacePool: WorkspacePool, xcodeInspector: ThreadSafeAccessToXcodeInspectorProtocol - ) -> Bool { + ) -> (accept: Bool, reason: String?) { let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode)) let tab = 48 - guard keycode == tab else { return false } - guard let fileURL = xcodeInspector.activeDocumentURL else { return false } - if event.flags.contains(.maskHelp) { return false } - if event.flags.contains(.maskShift) { return false } - if event.flags.contains(.maskControl) { return false } - if event.flags.contains(.maskCommand) { return false } - guard xcodeInspector.hasActiveXcode else { return false } - guard xcodeInspector.hasFocusedEditor else { return false } - guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) else { return false } - if filespace.presentingSuggestion == nil { return false } - return true + guard keycode == tab else { return (false, nil) } + if event.flags.contains(.maskHelp) { return (false, nil) } + if event.flags.contains(.maskShift) { return (false, nil) } + if event.flags.contains(.maskControl) { return (false, nil) } + if event.flags.contains(.maskCommand) { return (false, nil) } + guard xcodeInspector.hasActiveXcode else { + return (false, "No active Xcode") + } + guard xcodeInspector.hasFocusedEditor else { + return (false, "No focused editor") + } + guard let fileURL = xcodeInspector.activeDocumentURL else { + return (false, "No active document") + } + guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) else { + return (false, "No filespace") + } + if filespace.presentingSuggestion == nil { + return (false, "No suggestion") + } + return (true, nil) } } diff --git a/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift b/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift index 545e488..7046970 100644 --- a/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift +++ b/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift @@ -15,12 +15,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertTrue( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (true, nil) ) } @@ -28,17 +28,36 @@ class TabToAcceptSuggestionTests: XCTestCase { func test_should_not_accept_without_suggestion() { let fileURL = URL(string: "file:///test")! let workspacePool = FakeWorkspacePool() + workspacePool.setTestFile(fileURL: fileURL, skipSuggestion: true) let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( activeDocumentURL: fileURL, hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, "No suggestion") + ) + } + + @WorkspaceActor + func test_should_not_accept_without_filespace() { + let fileURL = URL(string: "file:///test")! + let workspacePool = FakeWorkspacePool() + let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( + activeDocumentURL: fileURL, + hasActiveXcode: true, + hasFocusedEditor: true + ) + assertEqual( + TabToAcceptSuggestion.shouldAcceptSuggestion( + event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, + workspacePool: workspacePool, + xcodeInspector: xcodeInspector + ), (false, "No filespace") ) } @@ -52,12 +71,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: false ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, "No focused editor") ) } @@ -71,12 +90,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: false, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, "No active Xcode") ) } @@ -90,12 +109,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, "No active document") ) } @@ -109,12 +128,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48, flags: .maskShift), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, nil) ) } @@ -128,12 +147,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48, flags: .maskCommand), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, nil) ) } @@ -147,12 +166,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48, flags: .maskControl), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, nil) ) } @@ -166,12 +185,12 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(48, flags: .maskHelp), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, nil) ) } @@ -185,16 +204,25 @@ class TabToAcceptSuggestionTests: XCTestCase { hasActiveXcode: true, hasFocusedEditor: true ) - XCTAssertFalse( + assertEqual( TabToAcceptSuggestion.shouldAcceptSuggestion( event: createEvent(50), workspacePool: workspacePool, xcodeInspector: xcodeInspector - ) + ), (false, nil) ) } } +private func assertEqual( + _ result: (Bool, String?), + _ expected: (Bool, String?) +) { + if result != expected { + XCTFail("Expected \(expected), got \(result)") + } +} + private func createEvent(_ keyCode: CGKeyCode, flags: CGEventFlags = []) -> CGEvent { let event = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true)! event.flags = flags @@ -212,9 +240,10 @@ private class FakeWorkspacePool: WorkspacePool { private var filespace: Filespace? @WorkspaceActor - func setTestFile(fileURL: URL) { + func setTestFile(fileURL: URL, skipSuggestion: Bool = false) { self.fileURL = fileURL self.filespace = Filespace(fileURL: fileURL, onSave: {_ in }, onClose: {_ in }) + if skipSuggestion { return } guard let filespace = self.filespace else { return } filespace.setSuggestions([.init(id: "id", text: "test", position: .zero, range: .zero)]) } diff --git a/README.md b/README.md index cf569b5..341a98a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Demo of GitHub Copilot for Xcode -GitHub Copilot for Xcode is macOS application and Xcode extension that enables -using GitHub Copilot code completions in Xcode. +[GitHub Copilot](https://github.com/features/copilot) is an AI pair programmer +that helps you write code faster and smarter. Copilot for Xcode is an +Xcode extension that provides inline coding suggestions as you type. ## Beta Preview Policy @@ -62,6 +63,16 @@ As per [GitHub's Terms of Service](https://docs.github.com/en/github/site-policy This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms. +## Privacy + +Your code is yours. We follow responsible practices in accordance with our +[Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement) +to ensure that your code snippets will not be used as suggested code for other +users of GitHub Copilot. + +To get the latest security fixes, please use the latest version of the GitHub +Copilot for Xcode. + ## Support We’d love to get your help in making GitHub Copilot better! If you have