Skip to content

Commit 63b5bca

Browse files
Pre-release 0.22.73
1 parent 0c92249 commit 63b5bca

13 files changed

Lines changed: 1073 additions & 0 deletions
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import AppKit
2+
import Client
3+
import GitHubCopilotService
4+
import Preferences
5+
import SharedUIComponents
6+
import SuggestionBasic
7+
import SwiftUI
8+
9+
struct SignInResponse {
10+
let userCode: String
11+
let verificationURL: URL
12+
}
13+
14+
struct GitHubCopilotView: View {
15+
static var copilotAuthService: GitHubCopilotAuthServiceType?
16+
17+
class Settings: ObservableObject {
18+
@AppStorage("username") var username: String = ""
19+
@AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear)
20+
var disableGitHubCopilotSettingsAutoRefreshOnAppear
21+
init() {}
22+
}
23+
24+
@Environment(\.openURL) var openURL
25+
@Environment(\.toast) var toast
26+
@StateObject var settings = Settings()
27+
28+
@State var status: GitHubCopilotAccountStatus?
29+
@State var signInResponse: SignInResponse?
30+
@State var version: String?
31+
@State var isRunningAction: Bool = false
32+
@State var isSignInAlertPresented = false
33+
@State var xcodeBetaAccessAlert = false
34+
@State var waitingForSignIn = false
35+
36+
func getGitHubCopilotAuthService() throws -> GitHubCopilotAuthServiceType {
37+
if let service = Self.copilotAuthService { return service }
38+
let service = try GitHubCopilotService()
39+
Self.copilotAuthService = service
40+
return service
41+
}
42+
43+
var body: some View {
44+
HStack {
45+
VStack(alignment: .leading, spacing: 8) {
46+
Text("Language Server Version: \(version ?? "Loading..")")
47+
.alert(isPresented: $xcodeBetaAccessAlert) {
48+
Alert(
49+
title: Text("Xcode Beta Access Not Granted"),
50+
message: Text(
51+
"Logged in user does not have access to GitHub Copilot for Xcode"
52+
),
53+
dismissButton: .default(Text("Close"))
54+
)
55+
}
56+
57+
if waitingForSignIn {
58+
Text("Status: Waiting for GitHub authentication")
59+
} else {
60+
Text("""
61+
Status: \(status?.description ?? "Loading..")\
62+
\(xcodeBetaAccessAlert ? " - Xcode Beta Access Not Granted" : "")
63+
""")
64+
}
65+
66+
HStack(alignment: .center) {
67+
Button("Refresh") {
68+
checkStatus()
69+
}
70+
if waitingForSignIn {
71+
Button("Cancel") { cancelWaiting() }
72+
} else if status == .notSignedIn {
73+
Button("Sign In") { signIn() }
74+
.alert(
75+
signInResponse?.userCode ?? "",
76+
isPresented: $isSignInAlertPresented,
77+
presenting: signInResponse) { _ in
78+
Button("Cancel", role: .cancel, action: {})
79+
Button("Copy Code and Open", action: copyAndOpen)
80+
} message: { response in
81+
Text("""
82+
Please enter the above code in the \
83+
GitHub website to authorize your \
84+
GitHub account with Copilot for Xcode.
85+
86+
\(response?.verificationURL.absoluteString ?? "")
87+
""")
88+
}
89+
}
90+
if status == .ok || status == .alreadySignedIn ||
91+
status == .notAuthorized
92+
{
93+
Button("Sign Out") { signOut() }
94+
}
95+
if isRunningAction || waitingForSignIn {
96+
ActivityIndicatorView()
97+
}
98+
}
99+
.opacity(isRunningAction ? 0.8 : 1)
100+
.disabled(isRunningAction)
101+
}
102+
.padding()
103+
104+
Spacer()
105+
}
106+
.onAppear {
107+
if isPreview { return }
108+
if settings.disableGitHubCopilotSettingsAutoRefreshOnAppear { return }
109+
checkStatus()
110+
}
111+
.textFieldStyle(.roundedBorder)
112+
.onReceive(FeatureFlagNotifierImpl.shared.featureFlagsDidChange) { flags in
113+
self.xcodeBetaAccessAlert = flags.x != true
114+
}
115+
}
116+
117+
func checkStatus() {
118+
Task {
119+
isRunningAction = true
120+
defer { isRunningAction = false }
121+
do {
122+
let service = try getGitHubCopilotAuthService()
123+
status = try await service.checkStatus()
124+
version = try await service.version()
125+
isRunningAction = false
126+
127+
if status != .ok, status != .notSignedIn {
128+
toast(
129+
"GitHub Copilot status is not \"ok\". Please check if you have a valid GitHub Copilot subscription.",
130+
131+
.error
132+
)
133+
}
134+
} catch {
135+
toast(error.localizedDescription, .error)
136+
}
137+
}
138+
}
139+
140+
func signIn() {
141+
Task {
142+
isRunningAction = true
143+
defer { isRunningAction = false }
144+
do {
145+
let service = try getGitHubCopilotAuthService()
146+
let (uri, userCode) = try await service.signInInitiate()
147+
guard let url = URL(string: uri) else {
148+
toast("Verification URI is incorrect.", .error)
149+
return
150+
}
151+
self.signInResponse = .init(userCode: userCode, verificationURL: url)
152+
isSignInAlertPresented = true
153+
} catch {
154+
toast(error.localizedDescription, .error)
155+
}
156+
}
157+
}
158+
159+
func copyAndOpen() {
160+
waitingForSignIn = true
161+
guard let signInResponse else {
162+
toast("Missing sign in details.", .error)
163+
return
164+
}
165+
// Copy the device code to the clipboard
166+
let pasteboard = NSPasteboard.general
167+
pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
168+
pasteboard.setString(signInResponse.userCode, forType: NSPasteboard.PasteboardType.string)
169+
toast("Sign-in code \(signInResponse.userCode) copied", .info)
170+
// Open verification URL in default browser
171+
openURL(signInResponse.verificationURL)
172+
// Wait for signInConfirm response
173+
waitForSignIn()
174+
}
175+
176+
func waitForSignIn() {
177+
Task {
178+
do {
179+
guard waitingForSignIn else { return }
180+
guard let signInResponse else {
181+
waitingForSignIn = false
182+
return
183+
}
184+
let service = try getGitHubCopilotAuthService()
185+
let (username, status) = try await service.signInConfirm(userCode: signInResponse.userCode)
186+
waitingForSignIn = false
187+
self.settings.username = username
188+
self.status = status
189+
} catch let error as GitHubCopilotError {
190+
if case .languageServerError(.timeout) = error {
191+
// TODO figure out how to extend the default timeout on an LSP request
192+
// Until then, reissue request
193+
waitForSignIn()
194+
return
195+
}
196+
throw error
197+
} catch {
198+
toast(error.localizedDescription, .error)
199+
}
200+
}
201+
202+
}
203+
204+
func cancelWaiting() {
205+
waitingForSignIn = false
206+
}
207+
208+
func signOut() {
209+
Task {
210+
isRunningAction = true
211+
defer { isRunningAction = false }
212+
do {
213+
let service = try getGitHubCopilotAuthService()
214+
status = try await service.signOut()
215+
} catch {
216+
toast(error.localizedDescription, .error)
217+
}
218+
}
219+
}
220+
221+
func refreshConfiguration() {
222+
NotificationCenter.default.post(
223+
name: .gitHubCopilotShouldRefreshEditorInformation,
224+
object: nil
225+
)
226+
227+
Task {
228+
let service = try getService()
229+
do {
230+
try await service.postNotification(
231+
name: Notification.Name
232+
.gitHubCopilotShouldRefreshEditorInformation.rawValue
233+
)
234+
} catch {
235+
toast(error.localizedDescription, .error)
236+
}
237+
}
238+
}
239+
}
240+
241+
struct ActivityIndicatorView: NSViewRepresentable {
242+
func makeNSView(context _: Context) -> NSProgressIndicator {
243+
let progressIndicator = NSProgressIndicator()
244+
progressIndicator.style = .spinning
245+
progressIndicator.controlSize = .small
246+
progressIndicator.startAnimation(nil)
247+
return progressIndicator
248+
}
249+
250+
func updateNSView(_: NSProgressIndicator, context _: Context) {
251+
// No-op
252+
}
253+
}
254+
255+
struct CopilotView_Previews: PreviewProvider {
256+
static var previews: some View {
257+
VStack(alignment: .leading, spacing: 8) {
258+
GitHubCopilotView(status: .notSignedIn, version: "1.0.0")
259+
GitHubCopilotView(status: .alreadySignedIn, isRunningAction: true)
260+
GitHubCopilotView(settings: .init(), status: .alreadySignedIn, xcodeBetaAccessAlert: true)
261+
GitHubCopilotView(settings: .init(), status: .notSignedIn, waitingForSignIn: true)
262+
}
263+
.padding(.all, 8)
264+
.previewLayout(.sizeThatFits)
265+
}
266+
}
267+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Preferences
2+
import SharedUIComponents
3+
import SwiftUI
4+
import XPCShared
5+
import Toast
6+
import Client
7+
8+
struct SuggesionSettingProxyView: View {
9+
10+
class Settings: ObservableObject {
11+
@AppStorage("username") var username: String = ""
12+
@AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost
13+
@AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort
14+
@AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername
15+
@AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword
16+
@AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL
17+
@AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI
18+
19+
init() {}
20+
}
21+
22+
@StateObject var settings = Settings()
23+
@Environment(\.toast) var toast
24+
25+
var body: some View {
26+
VStack(alignment: .leading) {
27+
SettingsDivider("Enterprise")
28+
29+
Form {
30+
TextField(
31+
text: $settings.gitHubCopilotEnterpriseURI,
32+
prompt: Text("Leave it blank if none is available.")
33+
) {
34+
Text("Auth provider URL")
35+
}
36+
}
37+
38+
SettingsDivider("Proxy")
39+
40+
Form {
41+
TextField(
42+
text: $settings.gitHubCopilotProxyHost,
43+
prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.")
44+
) {
45+
Text("Proxy host")
46+
}
47+
TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) {
48+
Text("Proxy port")
49+
}
50+
TextField(text: $settings.gitHubCopilotProxyUsername) {
51+
Text("Proxy username")
52+
}
53+
SecureField(text: $settings.gitHubCopilotProxyPassword) {
54+
Text("Proxy password")
55+
}
56+
Toggle("Proxy strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL)
57+
58+
Button("Refresh configurations") {
59+
refreshConfiguration()
60+
}.padding(.top, 6)
61+
}
62+
}
63+
.textFieldStyle(.roundedBorder)
64+
}
65+
66+
func refreshConfiguration() {
67+
NotificationCenter.default.post(
68+
name: .gitHubCopilotShouldRefreshEditorInformation,
69+
object: nil
70+
)
71+
72+
Task {
73+
let service = try getService()
74+
do {
75+
try await service.postNotification(
76+
name: Notification.Name
77+
.gitHubCopilotShouldRefreshEditorInformation.rawValue
78+
)
79+
} catch {
80+
toast(error.localizedDescription, .error)
81+
}
82+
}
83+
}
84+
}
85+
86+
#Preview {
87+
SuggesionSettingProxyView()
88+
}

0 commit comments

Comments
 (0)