diff --git a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift b/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift index ee90c35..bcd0adf 100644 --- a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift +++ b/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift @@ -1,17 +1,42 @@ +import Combine import SwiftUI +import Toast struct EnterpriseSection: View { @AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI + @Environment(\.toast) var toast var body: some View { SettingsSection(title: "Enterprise") { SettingsTextField( title: "Auth provider URL", - prompt: "Leave it blank if none is available.", - text: $gitHubCopilotEnterpriseURI + prompt: "https://your-enterprise.ghe.com", + text: DebouncedBinding($gitHubCopilotEnterpriseURI, handler: urlChanged).binding ) } } + + func urlChanged(_ url: String) { + if !url.isEmpty { + validateAuthURL(url) + } + NotificationCenter.default.post( + name: .gitHubCopilotShouldRefreshEditorInformation, + object: nil + ) + } + + func validateAuthURL(_ url: String) { + let maybeURL = URL(string: url) + guard let parsedURl = maybeURL else { + toast("Invalid URL", .error) + return + } + if parsedURl.scheme != "https" { + toast("URL scheme must be https://", .error) + return + } + } } #Preview { diff --git a/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift b/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift index 27b1ac2..168bdb1 100644 --- a/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift +++ b/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift @@ -15,33 +15,30 @@ struct ProxySection: View { SettingsTextField( title: "Proxy URL", prompt: "http://host:port", - text: $gitHubCopilotProxyUrl + text: wrapBinding($gitHubCopilotProxyUrl) ) SettingsTextField( title: "Proxy username", prompt: "username", - text: $gitHubCopilotProxyUsername + text: wrapBinding($gitHubCopilotProxyUsername) ) SettingsSecureField( title: "Proxy password", prompt: "password", - text: $gitHubCopilotProxyPassword + text: wrapBinding($gitHubCopilotProxyPassword) ) SettingsToggle( title: "Proxy strict SSL", - isOn: $gitHubCopilotUseStrictSSL + isOn: wrapBinding($gitHubCopilotUseStrictSSL) ) - } footer: { - HStack { - Spacer() - Button("Refresh configurations") { - refreshConfiguration() - } - } } } - func refreshConfiguration() { + private func wrapBinding(_ b: Binding) -> Binding { + DebouncedBinding(b, handler: refreshConfiguration).binding + } + + func refreshConfiguration(_: Any) { NotificationCenter.default.post( name: .gitHubCopilotShouldRefreshEditorInformation, object: nil diff --git a/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift b/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift index 8d0b51c..43a4aa8 100644 --- a/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift +++ b/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift @@ -6,7 +6,6 @@ struct CopilotConnectionView: View { @Environment(\.toast) var toast @StateObject var viewModel = GitHubCopilotViewModel() - @State var waitingForSignIn = false let store: StoreOf var body: some View { @@ -24,13 +23,13 @@ struct CopilotConnectionView: View { title: "GitHub Account Status Permissions", subtitle: "GitHub Connection: \(viewModel.status?.description ?? "Loading...")" ) { - if viewModel.isRunningAction || waitingForSignIn { + if viewModel.isRunningAction || viewModel.waitingForSignIn { ProgressView().controlSize(.small) } Button("Refresh Connection") { viewModel.checkStatus() } - if waitingForSignIn { + if viewModel.waitingForSignIn { Button("Cancel") { viewModel.cancelWaiting() } diff --git a/Core/Sources/HostApp/SharedComponents/DebouncedBinding.swift b/Core/Sources/HostApp/SharedComponents/DebouncedBinding.swift new file mode 100644 index 0000000..6b4224b --- /dev/null +++ b/Core/Sources/HostApp/SharedComponents/DebouncedBinding.swift @@ -0,0 +1,25 @@ +import Combine +import SwiftUI + +class DebouncedBinding { + private let subject = PassthroughSubject() + private let cancellable: AnyCancellable + private let wrappedBinding: Binding + + init(_ binding: Binding, handler: @escaping (T) -> Void) { + self.wrappedBinding = binding + self.cancellable = subject + .debounce(for: .seconds(1.0), scheduler: RunLoop.main) + .sink { handler($0) } + } + + var binding: Binding { + return Binding( + get: { self.wrappedBinding.wrappedValue }, + set: { + self.wrappedBinding.wrappedValue = $0 + self.subject.send($0) + } + ) + } +} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4c8818a..877cdd9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,8 +6,12 @@ Requires Node installed and `npm` available on your system path, e.g. ```sh sudo ln -s `which npm` /usr/local/bin +sudo ln -s `which node` /usr/local/bin ``` +For context, this is used by an Xcode run script as part of the build. Run +scripts use a very limited path to resolve commands. + ## Targets ### Copilot for Xcode diff --git a/ExtensionService/AuthStatusChecker.swift b/ExtensionService/AuthStatusChecker.swift index d144ed9..6da7071 100644 --- a/ExtensionService/AuthStatusChecker.swift +++ b/ExtensionService/AuthStatusChecker.swift @@ -15,11 +15,11 @@ class AuthStatusChecker { Task { do { let status = try await self.getCurrentAuthStatus() - DispatchQueue.main.async { + Task { @MainActor in notify(status.description, status == .ok) } } catch { - DispatchQueue.main.async { + Task { @MainActor in notify("\(error)", false) } } diff --git a/README.md b/README.md index 82eb3ea..255c74f 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,14 @@ Use of the GitHub Copilot Xcode Extension is subject to [GitHub's Pre-Release Te Screenshot of downloaded from the internet warning

-1. A background item will be added for the application to be able to start itself when Xcode starts. +1. A background item will be added to enable Copilot to start when Xcode is opened.

Screenshot of background item

-1. Two permissions are required: `Accessibility` and `Xcode Source Editor Extension`. +1. Two permissions are required: `Accessibility` and `Xcode Source Editor + Extension`. For more on why these permissions are required see + [TROUBLESHOOTING.md](./TROUBLESHOOTING.md). The first time the application is run the `Accessibility` permission should be requested: @@ -57,7 +59,9 @@ Use of the GitHub Copilot Xcode Extension is subject to [GitHub's Pre-Release Te Screenshot of extension permission

-1. After granting the extension permission, please restart Xcode so the `Github Copilot` menu is available under the Xcode `Editor` menu. +1. After granting the extension permission, please restart Xcode to ensure the + `Github Copilot` menu is available and not disabled under the Xcode `Editor` + menu.

Screenshot of Xcode Editor GitHub Copilot menu item @@ -71,7 +75,16 @@ Use of the GitHub Copilot Xcode Extension is subject to [GitHub's Pre-Release Te Screenshot of sign-in popup

-1. To install updates, click `Check for Updates` from the menu item or in the settings application. After installing a new version, Xcode must be restarted to use the new version correctly. New versions can also be installed from `dmg` files downloaded from the releases page. When installing a new version via `dmg`, the application must be run manually the first time to accept the downloaded from the internet warning. +1. To install updates, click `Check for Updates` from the menu item or in the + settings application. + + After installing a new version, Xcode must be restarted to use the new + version correctly. + + New versions can also be installed from `dmg` files downloaded from the + releases page. When installing a new version via `dmg`, the application must + be run manually the first time to accept the downloaded from the internet + warning. 1. To avoid confusion, we recommend disabling `Predictive code completion` under `Xcode` > `Preferences` > `Text Editing` > `Editing`. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 1d127f8..98fdca7 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -15,7 +15,8 @@ common issues: then Xcode needs to be restarted to enable the extension. 3. Need more help? If these steps don't resolve the issue, please [open an - issue](https://github.com/github/CopilotForXcode/issues/new/choose). + issue](https://github.com/github/CopilotForXcode/issues/new/choose). Make + sure to [include logs](#logs) and any other relevant information. ## Extension Permission @@ -34,8 +35,33 @@ Or you can navigate to the permission manually depending on your OS version: ## Accessibility Permission -GitHub Copilot for Xcode requires accessibility permission to receive -information from the active Xcode editor. +GitHub Copilot for Xcode requires the accessibility permission to receive +real-time updates from the active Xcode editor. [The XcodeKit +API](https://developer.apple.com/documentation/xcodekit) +enabled by the Xcode Source Editor extension permission only provides +information when manually triggered by the user. In order to generate +suggestions as you type, the accessibility permission is used read the +Xcode editor content in real-time. + +The accessibility permission is also used to accept suggestions when `tab` is +pressed. + +The accessibility permission is __not__ used to read or write to any +applications besides Xcode. There are no granular options for the permission, +but you can audit the usage in this repository: search for `CGEvent` and `AX`*. Enable in System Settings under `Privacy & Security` > `Accessibility` > `GitHub Copilot for Xcode Extension` and turn on the toggle. + +## Logs + +Logs can be found in `~/Library/Logs/GitHubCopilot/` the most recent log file +is: + +``` +~/Library/Logs/GitHubCopilot/github-copilot-for-xcode.log +``` + +To enable verbose logging, open the GitHub Copilot for Xcode settings and enable +`Verbose Logging` in the `Advanced` tab. After enabling verbose logging, restart +Copilot for Xcode for the change to take effect. diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 159384b..1c7f252 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -67,13 +67,11 @@ public func editorConfiguration() -> JSONValue { d["proxyAuthorization"] = .string(proxyAuthorization) } d["proxyStrictSSL"] = .bool(UserDefaults.shared.value(for: \.gitHubCopilotUseStrictSSL)) - if d.isEmpty { return nil } return .hash(d) } var authProvider: JSONValue? { let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) - if enterpriseURI.isEmpty { return nil } return .hash([ "uri": .string(enterpriseURI) ]) } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index a80064e..a2b6578 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -314,7 +314,7 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, tabSize: tabSize, insertSpaces: !usesTabsForIndentation ), - context: .init(triggerKind: .automatic) + context: .init(triggerKind: .invoked) ))) .items .compactMap { (item: _) -> CodeSuggestion? in