diff --git a/Copilot for Xcode/App.swift b/Copilot for Xcode/App.swift index 094e32d..a1cf045 100644 --- a/Copilot for Xcode/App.swift +++ b/Copilot for Xcode/App.swift @@ -1,6 +1,7 @@ import Client import HostApp import LaunchAgentManager +import SharedUIComponents import SwiftUI import UpdateChecker import XPCShared @@ -21,6 +22,7 @@ struct CopilotForXcodeApp: App { UserDefaults.setupDefaultSettings() } .environment(\.updateChecker, UpdateChecker(hostBundle: Bundle.main)) + .copilotIntroSheet() } } } diff --git a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json new file mode 100644 index 0000000..2e35661 --- /dev/null +++ b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "copilot.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg new file mode 100644 index 0000000..8284dce --- /dev/null +++ b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json new file mode 100644 index 0000000..1f7fbbe --- /dev/null +++ b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties": { + "preserves-vector-representation": true + } +} diff --git a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg new file mode 100644 index 0000000..1520416 --- /dev/null +++ b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggesionSettingProxyView.swift b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggesionSettingProxyView.swift index caa217b..7f8488c 100644 --- a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggesionSettingProxyView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggesionSettingProxyView.swift @@ -9,8 +9,7 @@ struct SuggesionSettingProxyView: View { class Settings: ObservableObject { @AppStorage("username") var username: String = "" - @AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost - @AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort + @AppStorage(\.gitHubCopilotProxyUrl) var gitHubCopilotProxyUrl @AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername @AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword @AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL @@ -39,13 +38,10 @@ struct SuggesionSettingProxyView: View { Form { TextField( - text: $settings.gitHubCopilotProxyHost, - prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.") + text: $settings.gitHubCopilotProxyUrl, + prompt: Text("http://host:port") ) { - Text("Proxy host") - } - TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) { - Text("Proxy port") + Text("Proxy URL") } TextField(text: $settings.gitHubCopilotProxyUsername) { Text("Proxy username") diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a7b16c4..4c8818a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,22 +8,6 @@ Requires Node installed and `npm` available on your system path, e.g. sudo ln -s `which npm` /usr/local/bin ``` -## Local Language Server - -To run the language server locally create a `Config.local.xcconfig` file with two config values: - -```xcconfig -LANGUAGE_SERVER_PATH=~/code/copilot-client -NODE_PATH=/opt/path/to/node -``` - -`LANGUAGE_SERVER_PATH` should point to the path where the copilot-client repo is -checked out and `$(LANGUAGE_SERVER_PATH)/dist/language-server.js` must exist -(run `npm run build`). - -`NODE_PATH` should point to where node is installed. It can be omitted if -`/usr/bin/env node` will resolves directly. - ## Targets ### Copilot for Xcode @@ -79,4 +63,4 @@ The source code mostly follows the [Ray Wenderlich Style Guide](https://github.c ## App Versioning -The app version and all targets' version in controlled by `Version.xcconfig`. \ No newline at end of file +The app version and all targets' version in controlled by `Version.xcconfig`. diff --git a/Docs/demo.gif b/Docs/demo.gif index 5310fa1..6e10eca 100644 Binary files a/Docs/demo.gif and b/Docs/demo.gif differ diff --git a/README.md b/README.md index 1e9b13c..cf569b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GitHub Copilot For Xcode -Demo of GitHub Copilot for Xcode +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. diff --git a/Server/package-lock.json b/Server/package-lock.json index 35e4cce..936d91a 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -8,13 +8,13 @@ "name": "@github/copilot-xcode", "version": "0.0.1", "dependencies": { - "@github/copilot-language-server": "^1.234.0" + "@github/copilot-language-server": "^1.235.0" } }, "node_modules/@github/copilot-language-server": { - "version": "1.234.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.234.0.tgz", - "integrity": "sha512-uNjSZZawr5uNE6+3IVNI0/mnImBqDFt8bEdz7zNLp3a8t0LcCJPA7rW0GR3r3LZ9wuIwjgz+XuS938q9lmN1Jg==", + "version": "1.235.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.235.0.tgz", + "integrity": "sha512-QvBoh0qx9yBPBKxxfL2oWxrL5Kl4scniB7QpUJmIkudagc/23epZIo1fZXwHeYYzmnmOpjMATE5PVOieokPXWA==", "bin": { "copilot-language-server": "dist/language-server.js" } diff --git a/Server/package.json b/Server/package.json index d281b5d..32e9d20 100644 --- a/Server/package.json +++ b/Server/package.json @@ -4,6 +4,6 @@ "description": "Package for downloading @github/copilot-language-server", "private": true, "dependencies": { - "@github/copilot-language-server": "^1.234.0" + "@github/copilot-language-server": "^1.235.0" } } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 4263774..159384b 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -49,72 +49,41 @@ public struct GitHubCopilotCodeSuggestion: Codable, Equatable { public var displayText: String } -enum GitHubCopilotRequest { - // TODO migrate from setEditorInfo to didConfigurationChange - struct SetEditorInfo: GitHubCopilotRequestType { - struct Response: Codable {} - - let versionNumber = JSONValue(stringLiteral: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") - let xcodeVersion = JSONValue(stringLiteral: SystemInfo().xcodeVersion() ?? "") - - var networkProxy: JSONValue? { - let host = UserDefaults.shared.value(for: \.gitHubCopilotProxyHost) - if host.isEmpty { return nil } - var port = UserDefaults.shared.value(for: \.gitHubCopilotProxyPort) - if port.isEmpty { port = "80" } - let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) - if username.isEmpty { - return .hash([ - "host": .string(host), - "port": .number(Double(Int(port) ?? 80)), - "rejectUnauthorized": .bool(UserDefaults.shared - .value(for: \.gitHubCopilotUseStrictSSL)), - ]) - } else { - return .hash([ - "host": .string(host), - "port": .number(Double(Int(port) ?? 80)), - "rejectUnauthorized": .bool(UserDefaults.shared - .value(for: \.gitHubCopilotUseStrictSSL)), - "username": .string(username), - "password": .string(UserDefaults.shared - .value(for: \.gitHubCopilotProxyPassword)), - - ]) - } - } - - var authProvider: JSONValue? { - var dict: [String: JSONValue] = [:] - let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) - if !enterpriseURI.isEmpty { - dict["url"] = .string(enterpriseURI) - } +public func editorConfiguration() -> JSONValue { + var proxyAuthorization: String? { + let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) + if username.isEmpty { return nil } + let password = UserDefaults.shared.value(for: \.gitHubCopilotProxyPassword) + return "\(username):\(password)" + } - if dict.isEmpty { return nil } - return .hash(dict) + var http: JSONValue? { + var d: [String: JSONValue] = [:] + let proxy = UserDefaults.shared.value(for: \.gitHubCopilotProxyUrl) + if !proxy.isEmpty { + d["proxy"] = .string(proxy) } - - var request: ClientRequest { - var dict: [String: JSONValue] = [ - "editorInfo": .hash([ - "name": "Xcode", - "version": xcodeVersion, - ]), - "editorPluginInfo": .hash([ - "name": "copilot-xcode", - "version": versionNumber, - ]), - ] - - dict["authProvider"] = authProvider - dict["networkProxy"] = networkProxy - - return .custom("setEditorInfo", .hash(dict)) + if let proxyAuthorization = proxyAuthorization { + 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) ]) } + var d: [String: JSONValue] = [:] + if let http { d["http"] = http } + if let authProvider { d["github-enterprise"] = authProvider } + return .hash(d) +} + +enum GitHubCopilotRequest { struct GetVersion: GitHubCopilotRequestType { struct Response: Codable { var version: String diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 24dd5bf..806fd90 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -172,10 +172,6 @@ public class GitHubCopilotBaseService { return InitializeParams( processId: Int(ProcessInfo.processInfo.processIdentifier), - clientInfo: .init( - name: "copilot-xcode", - version: "1.5.0.5206-nightly" - ), locale: nil, rootPath: projectRootURL.path, rootUri: projectRootURL.path, @@ -188,6 +184,7 @@ public class GitHubCopilotBaseService { "name": "copilot-xcode", "version": versionNumber, ], + "editorConfiguration": editorConfiguration(), ], capabilities: capabilities, trace: .off, @@ -207,11 +204,13 @@ public class GitHubCopilotBaseService { let notifications = NotificationCenter.default .notifications(named: .gitHubCopilotShouldRefreshEditorInformation) Task { [weak self] in - _ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) - for await _ in notifications { guard self != nil else { return } - _ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) + _ = try? await server.sendNotification( + .workspaceDidChangeConfiguration( + .init(settings: editorConfiguration()) + ) + ) } } } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 92c4dfb..eb1b3b7 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -94,10 +94,23 @@ public struct UserDefaultPreferenceKeys { ) // MARK: Completion Hint Shown + public let completionHintShown = PreferenceKey( defaultValue: false, key: "CompletionHintShown" ) + + // MARK: First Time Intro Interface + + public let introLastShownVersion = PreferenceKey( + defaultValue: "", + key: "IntroLastShownVersion" + ) + + public let hideIntro = PreferenceKey( + defaultValue: false, + key: "HideIntro" + ) } // MARK: - Prompt to Code @@ -512,13 +525,9 @@ public extension UserDefaultPreferenceKeys { // MARK: - Feature public extension UserDefaultPreferenceKeys { - - var gitHubCopilotProxyHost: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotProxyHost") - } - - var gitHubCopilotProxyPort: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotProxyPort") + + var gitHubCopilotProxyUrl: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotProxyUrl") } var gitHubCopilotUseStrictSSL: PreferenceKey { diff --git a/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift b/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift new file mode 100644 index 0000000..a6008b9 --- /dev/null +++ b/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift @@ -0,0 +1,113 @@ +import SwiftUI +import AppKit + +struct CopilotIntroItem: View { + let heading: String + let text: String + let image: Image + + public init(imageName: String, heading: String, text: String) { + self.init(imageObject: Image(imageName), heading: heading, text: text) + } + + public init(systemImage: String, heading: String, text: String) { + self.init(imageObject: Image(systemName: systemImage), heading: heading, text: text) + } + + public init(imageObject: Image, heading: String, text: String) { + self.heading = heading + self.text = text + self.image = imageObject + } + + var body: some View { + HStack(spacing: 16) { + image + .resizable() + .renderingMode(.template) + .foregroundColor(Color(red: 0.0353, green: 0.4118, blue: 0.8549)) + .scaledToFit() + .frame(width: 28, height: 28) + VStack(alignment: .leading, spacing: 5) { + Text(heading) + .font(.system(size: 11, weight: .bold)) + Text(text) + .font(.system(size: 11)) + .lineSpacing(3) + } + } + } +} + +public struct CopilotIntroSheet: View { + let content: Content + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + @AppStorage(\.hideIntro) var hideIntro + @AppStorage(\.introLastShownVersion) var introLastShownVersion + @State var isPresented = false + + public var body: some View { + content.sheet(isPresented: $isPresented) { + VStack { + Image(nsImage: NSImage(named: "AppIcon") ?? NSImage()) + .resizable() + .scaledToFit() + .frame(width: 64, height: 64) + .padding(.bottom, 24) + Text("Welcome to Copilot for Xcode!") + .font(.title) + .padding(.bottom, 45) + + VStack(alignment: .leading, spacing: 25) { + CopilotIntroItem( + imageName: "CopilotLogo", + heading: "In-line Code Suggestions", + text: "Copilot's code suggestions and text completion now available in Xcode. Just press Tab ⇥ to accept a suggestion." + ) + + CopilotIntroItem( + systemImage: "option", + heading: "Full Suggestion", + text: "Press Option ⌥ key to display the full suggestion. Only the first line of suggestions are shown inline." + ) + + CopilotIntroItem( + imageName: "GitHubMark", + heading: "GitHub Context", + text: "Copilot utilizes GitHub and project context to deliver smarter completions and personalized code suggestions relevant to your unique codebase." + ) + } + + Spacer() + + VStack(spacing: 8) { + Button(action: { isPresented = false }) { + Text("Continue") + .padding(.horizontal, 80) + .padding(.vertical, 6) + } + .buttonStyle(.borderedProminent) + + Toggle("Don't show again", isOn: $hideIntro) + .toggleStyle(.checkbox) + } + } + .padding(EdgeInsets(top: 50, leading: 50, bottom: 16, trailing: 50)) + .frame(width: 560, height: 528) + } + .task { + let neverShown = introLastShownVersion.isEmpty + isPresented = neverShown || !hideIntro + if isPresented { + hideIntro = neverShown ? true : hideIntro // default to hidden on first time + introLastShownVersion = appVersion + } + } + } +} + +public extension View { + func copilotIntroSheet() -> some View { + CopilotIntroSheet(content: self) + } +}