diff --git a/ChatGPTForXcode/ChatGPTForXcodeApp.swift b/ChatGPTForXcode/ChatGPTForXcodeApp.swift index afd2f23..ed843bc 100644 --- a/ChatGPTForXcode/ChatGPTForXcodeApp.swift +++ b/ChatGPTForXcode/ChatGPTForXcodeApp.swift @@ -10,10 +10,101 @@ import SwiftUI @main struct ChatGPTForXcodeApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate : AppDelegate var body: some Scene { - WindowGroup { - ConfigurationView() + MenuBarExtra { + Button { + NSApplication.shared.terminate(self) + } label: { + Text("Quit") + } + } label: { + Image(systemName: "bubble.left.fill") } - .windowResizability(.contentSize) + + } +} + +// chat-gpt-for-xcode:// + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + openConfigurationView() + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if !flag { + openConfigurationView() + } + return true + } + + func application(_ application: NSApplication, open urls: [URL]) { + print(urls) + if let url = urls.first { + openChatPanel(url: url) + } + } + + private func openConfigurationView() { + if NSApplication.shared.windows.filter({$0.identifier == .init("configurationWindow")}).first != nil { + return + } + let view = ConfigurationView() + let controller = NSHostingController(rootView: view) + + let window = NSPanel( + contentRect: .zero, + styleMask: [ + .closable, + .miniaturizable, + .titled, + ], + backing: .buffered, + defer: false) + window.identifier = .init("configurationWindow") + window.contentViewController = controller + window.toolbarStyle = .unified + window.toolbar = .init() + window.center() + window.makeKeyAndOrderFront(nil) + } + + let chatViewModel = ChatViewModel() + + private func openChatPanel(url: URL) { + guard let query = url.query, + let text = query.removingPercentEncoding + else { return } + + chatViewModel.messages.append(.init(text: text)) + + if NSApplication.shared.windows.filter({$0.identifier == .init("chatPanel")}).first != nil { + return + } + + let view = ChatView(viewModel: self.chatViewModel) + let controller = NSHostingController(rootView: view) + + let panel = NSPanel( + contentRect: .zero, + styleMask: [ + .closable, + .miniaturizable, + .nonactivatingPanel, + .titled, + .resizable + ], + backing: .buffered, + defer: false) + panel.identifier = .init("chatPanel") + panel.contentViewController = controller + panel.level = .floating + panel.collectionBehavior = [ + .canJoinAllSpaces, + .fullScreenAuxiliary + ] + panel.center() + panel.makeKeyAndOrderFront(nil) } } diff --git a/ChatGPTForXcode/ChatView.swift b/ChatGPTForXcode/ChatView.swift new file mode 100644 index 0000000..ca1dd61 --- /dev/null +++ b/ChatGPTForXcode/ChatView.swift @@ -0,0 +1,39 @@ +// +// ChatView.swift +// ChatGPTForXcode +// +// Created by 安部翔太 on 2023/03/25. +// + +import SwiftUI + +struct Message: Identifiable { + let id = UUID() + let text: String +} + +final class ChatViewModel: ObservableObject { + @Published var messages: [Message] = [] +} + +struct ChatView: View { + @StateObject var viewModel: ChatViewModel + var body: some View { + List(viewModel.messages) { message in + VStack(spacing: 4) { + Text(message.text) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + Divider() + } + } + .frame(minWidth: 200, minHeight: 200) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct ChatView_Previews: PreviewProvider { + static var previews: some View { + ChatView(viewModel: .init()) + } +} diff --git a/ChatGPTForXcode/ConfigurationView.swift b/ChatGPTForXcode/ConfigurationView.swift index aeabe58..f68cadf 100644 --- a/ChatGPTForXcode/ConfigurationView.swift +++ b/ChatGPTForXcode/ConfigurationView.swift @@ -9,16 +9,16 @@ import SwiftUI struct ConfigurationView: View { private let apiKeyRepository = APIKeyRepository() - private let languageRepository = LanguageRepository() + private let displayInFloatingWindowRepository = DisplayInFloatingWindowRepository() @State private var apiKey = "" - @State private var selectedLanguage = Language.english + @State private var displayInFloatingWindow = false var body: some View { NavigationStack { - VStack(spacing: 10) { + VStack(alignment: .leading, spacing: 10) { headline("1. Obtain your API Key from OpenAI.") link() @@ -30,15 +30,22 @@ struct ConfigurationView: View { headline("3. Specify the output language setting.") languagePicker() + + Divider() + .padding(.vertical, 4) + + floatingWindowToggle() } .padding(.init(top: 25, leading: 25, bottom: 25, trailing: 28)) - .frame(width: 400, height: 230, alignment: .center) + .frame(width: 400, height: 300, alignment: .center) .onAppear { apiKey = apiKeyRepository.getAPIKey() selectedLanguage = languageRepository.getSelectedLanguage() + displayInFloatingWindow = displayInFloatingWindowRepository.get() } .onChange(of: apiKey, perform: apiKeyRepository.saveAPIKey(apiKey:)) .onChange(of: selectedLanguage, perform: languageRepository.saveSelectedLanguage(language:)) + .onChange(of: displayInFloatingWindow, perform: displayInFloatingWindowRepository.save) .navigationTitle("ChatGPT for Xcode") .toolbar { toolbarButton() @@ -87,6 +94,15 @@ extension ConfigurationView { options: [NSApplication.AboutPanelOptionKey(rawValue: "Copyright"): "© 2023 Avis Inc"] ) } + + private func floatingWindowToggle() -> some View { + Toggle(isOn: $displayInFloatingWindow) { + Text("Present suggestions in Floating Window") + .font(.system(size: 14)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .toggleStyle(.switch) + } } struct ConfigurationView_Previews: PreviewProvider { diff --git a/ChatGPTForXcode/Info.plist b/ChatGPTForXcode/Info.plist index 3d4c1e5..304feaa 100644 --- a/ChatGPTForXcode/Info.plist +++ b/ChatGPTForXcode/Info.plist @@ -2,7 +2,18 @@ - IDEDidComputeMac32BitWarning - + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + chat-gpt-for-xcode + + + + IDEDidComputeMac32BitWarning + diff --git a/ChatGPTForXcodeEditorExtension/BaseCommand.swift b/ChatGPTForXcodeEditorExtension/BaseCommand.swift index b43a3f2..ee33f17 100644 --- a/ChatGPTForXcodeEditorExtension/BaseCommand.swift +++ b/ChatGPTForXcodeEditorExtension/BaseCommand.swift @@ -8,6 +8,7 @@ import Foundation import XcodeKit +import AppKit class BaseCommand: NSObject, XCSourceEditorCommand { func perform( @@ -46,10 +47,14 @@ class BaseCommand: NSObject, XCSourceEditorCommand { let apiKeyRepository = APIKeyRepository() let languageRepository = LanguageRepository() + + let displayInFloatingWindowRepository = DisplayInFloatingWindowRepository() let authToken = apiKeyRepository.getAPIKey() let language = languageRepository.getSelectedLanguage() + + let displayInFloatingWindow = displayInFloatingWindowRepository.get() let content = prompt(code, language: language) @@ -61,16 +66,25 @@ class BaseCommand: NSObject, XCSourceEditorCommand { .init(role: .user, content: content) ] ) - let indentSpace = String(repeating: " ", count: indentCount) - let markerComment = "\(indentSpace)// MARK: \(commandType.rawValue)" - var reviewComment = messageResult.choices.first?.message.content ?? "" - reviewComment = reviewComment - .split(separator: "\n") - .map { "\(indentSpace)/// \($0)" } - .joined(separator: "\n") - let comments = [markerComment, reviewComment] - let result = comments.joined(separator: "\n") - buffer.lines.insert(result, at: selection.start.line) + + if displayInFloatingWindow { + let encodedComment = (messageResult.choices.first?.message.content ?? "").addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + if let url = URL(string: "chat-gpt-for-xcode://chat?\(encodedComment)") { + NSWorkspace.shared.open(url) + } + } else { + let indentSpace = String(repeating: " ", count: indentCount) + let markerComment = "\(indentSpace)// MARK: \(commandType.rawValue)" + var reviewComment = messageResult.choices.first?.message.content ?? "" + reviewComment = reviewComment + .split(separator: "\n") + .map { "\(indentSpace)/// \($0)" } + .joined(separator: "\n") + let comments = [markerComment, reviewComment] + let result = comments.joined(separator: "\n") + buffer.lines.insert(result, at: selection.start.line) + } + completionHandler(nil) } catch { completionHandler(error) diff --git a/Shared/Infrastructure/Repository/DisplayInFloatingWindowRepository.swift b/Shared/Infrastructure/Repository/DisplayInFloatingWindowRepository.swift new file mode 100644 index 0000000..72463c4 --- /dev/null +++ b/Shared/Infrastructure/Repository/DisplayInFloatingWindowRepository.swift @@ -0,0 +1,23 @@ +// +// DisplayInFloatingWindowRepository.swift +// ChatGPTForXcode +// +// Created by 安部翔太 on 2023/03/26. +// + +import Foundation + +public struct DisplayInFloatingWindowRepository { + private let key = "displayInFloatingWindow" + + private let userDefaults = UserDefaults(suiteName: "com.ChatGPTForXcode.UserDefaults") + + func get() -> Bool { + let bool = userDefaults?.bool(forKey: key) + return bool ?? false + } + + func save(_ bool: Bool) { + userDefaults?.set(bool, forKey: key) + } +}