Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

結果をフローティングパネルで表示できるようにした #18

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
97 changes: 94 additions & 3 deletions ChatGPTForXcode/ChatGPTForXcodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
39 changes: 39 additions & 0 deletions ChatGPTForXcode/ChatView.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
24 changes: 20 additions & 4 deletions ChatGPTForXcode/ConfigurationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 13 additions & 2 deletions ChatGPTForXcode/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>chat-gpt-for-xcode</string>
</array>
</dict>
</array>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
34 changes: 24 additions & 10 deletions ChatGPTForXcodeEditorExtension/BaseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import XcodeKit
import AppKit

class BaseCommand: NSObject, XCSourceEditorCommand {
func perform(
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}