Skip to content

Commit 11a4762

Browse files
committed
Support prompt to code in widget
1 parent 2e13528 commit 11a4762

File tree

10 files changed

+529
-72
lines changed

10 files changed

+529
-72
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Combine
2+
import PromptToCodeService
3+
import SuggestionWidget
4+
5+
extension PromptToCodeProvider {
6+
convenience init(service: PromptToCodeService, onClosePromptToCode: @escaping () -> Void) {
7+
self.init(
8+
code: service.code,
9+
language: service.language.rawValue,
10+
description: "",
11+
startLineIndex: service.selectionRange.start.line
12+
)
13+
14+
var cancellables = Set<AnyCancellable>()
15+
16+
service.$code.sink(receiveValue: set(\.code)).store(in: &cancellables)
17+
service.$isResponding.sink(receiveValue: set(\.isResponding)).store(in: &cancellables)
18+
service.$description.sink(receiveValue: set(\.description)).store(in: &cancellables)
19+
service.$oldCode.map { $0 != nil }
20+
.sink(receiveValue: set(\.canRevert)).store(in: &cancellables)
21+
22+
onCancelTapped = { [cancellables] in
23+
_ = cancellables
24+
service.stopResponding()
25+
onClosePromptToCode()
26+
}
27+
28+
onRevertTapped = {
29+
service.revert()
30+
}
31+
32+
onRequirementSent = { [weak self] requirement in
33+
Task { [weak self] in
34+
do {
35+
try await service.modifyCode(prompt: requirement)
36+
} catch {
37+
Task { @MainActor [weak self] in
38+
self?.errorMessage = error.localizedDescription
39+
}
40+
}
41+
}
42+
}
43+
44+
onStopRespondingTap = {
45+
service.stopResponding()
46+
}
47+
48+
onAcceptSuggestionTapped = {
49+
Task { @ServiceActor in
50+
let handler = PseudoCommandHandler()
51+
await handler.acceptSuggestion()
52+
}
53+
}
54+
}
55+
56+
func set<T>(_ keyPath: WritableKeyPath<PromptToCodeProvider, T>) -> (T) -> Void {
57+
return { [weak self] value in
58+
Task { @MainActor [weak self] in
59+
self?[keyPath: keyPath] = value
60+
}
61+
}
62+
}
63+
}
Lines changed: 125 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,146 @@
11
import ChatService
2+
import CopilotModel
3+
import CopilotService
24
import Foundation
35
import OpenAIService
6+
import PromptToCodeService
47
import SuggestionWidget
58

9+
@ServiceActor
610
final class WidgetDataSource {
711
static let shared = WidgetDataSource()
812

9-
var globalChat: ChatService?
10-
var chats = [URL: ChatService]()
11-
var globalChatProvider: ChatProvider?
12-
var chatProviders = [URL: ChatProvider]()
13+
final class Chat {
14+
let chatService: ChatService
15+
let provider: ChatProvider
16+
public init(chatService: ChatService, provider: ChatProvider) {
17+
self.chatService = chatService
18+
self.provider = provider
19+
}
20+
}
21+
22+
final class PromptToCode {
23+
let promptToCodeService: PromptToCodeService
24+
let provider: PromptToCodeProvider
25+
public init(
26+
promptToCodeService: PromptToCodeService,
27+
provider: PromptToCodeProvider
28+
) {
29+
self.promptToCodeService = promptToCodeService
30+
self.provider = provider
31+
}
32+
}
33+
34+
private(set) var globalChat: Chat?
35+
private(set) var chats = [URL: Chat]()
36+
private(set) var promptToCodes = [URL: PromptToCode]()
1337

1438
private init() {}
1539

1640
@discardableResult
1741
func createChatIfNeeded(for url: URL) -> ChatService {
42+
let build = {
43+
let service = ChatService(chatGPTService: ChatGPTService())
44+
let provider = ChatProvider(
45+
service: service,
46+
fileURL: url,
47+
onCloseChat: { [weak self] in
48+
if UserDefaults.shared.value(for: \.useGlobalChat) {
49+
self?.globalChat = nil
50+
} else {
51+
self?.removeChat(for: url)
52+
}
53+
let presenter = PresentInWindowSuggestionPresenter()
54+
presenter.closeChatRoom(fileURL: url)
55+
},
56+
onSwitchContext: { [weak self] in
57+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
58+
UserDefaults.shared.set(!useGlobalChat, for: \.useGlobalChat)
59+
self?.createChatIfNeeded(for: url)
60+
let presenter = PresentInWindowSuggestionPresenter()
61+
presenter.presentChatRoom(fileURL: url)
62+
}
63+
)
64+
return Chat(chatService: service, provider: provider)
65+
}
66+
1867
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
19-
let chat: ChatService
20-
2168
if useGlobalChat {
22-
chat = globalChat ?? ChatService(chatGPTService: ChatGPTService())
23-
globalChat = chat
69+
if let globalChat {
70+
return globalChat.chatService
71+
}
72+
let newChat = build()
73+
globalChat = newChat
74+
return newChat.chatService
2475
} else {
25-
chat = chats[url] ?? ChatService(chatGPTService: ChatGPTService())
26-
chats[url] = chat
76+
if let chat = chats[url] {
77+
return chat.chatService
78+
}
79+
let newChat = build()
80+
chats[url] = newChat
81+
return newChat.chatService
82+
}
83+
}
84+
85+
@discardableResult
86+
func createPromptToCode(
87+
for url: URL,
88+
code: String,
89+
selectionRange: CursorRange,
90+
language: CopilotLanguage,
91+
identSize: Int = 4,
92+
usesTabsForIndentation: Bool = false
93+
) async -> PromptToCodeService {
94+
let build = {
95+
let service = PromptToCodeService(
96+
code: code,
97+
selectionRange: selectionRange,
98+
language: language,
99+
identSize: identSize,
100+
usesTabsForIndentation: usesTabsForIndentation
101+
)
102+
let provider = PromptToCodeProvider(
103+
service: service,
104+
onClosePromptToCode: { [weak self] in
105+
self?.removePromptToCode(for: url)
106+
let presenter = PresentInWindowSuggestionPresenter()
107+
presenter.closePromptToCode(fileURL: url)
108+
}
109+
)
110+
return PromptToCode(promptToCodeService: service, provider: provider)
27111
}
28-
return chat
112+
113+
let newPromptToCode = build()
114+
promptToCodes[url] = newPromptToCode
115+
return newPromptToCode.promptToCodeService
116+
}
117+
118+
func removeChat(for url: URL) {
119+
chats[url] = nil
120+
}
121+
122+
func removePromptToCode(for url: URL) {
123+
promptToCodes[url] = nil
124+
}
125+
126+
func cleanup(for url: URL) {
127+
removeChat(for: url)
128+
removePromptToCode(for: url)
29129
}
30130
}
31131

32132
extension WidgetDataSource: SuggestionWidgetDataSource {
33133
func suggestionForFile(at url: URL) async -> SuggestionProvider? {
34-
for workspace in await workspaces.values {
35-
if let filespace = await workspace.filespaces[url],
36-
let suggestion = await filespace.presentingSuggestion
134+
for workspace in workspaces.values {
135+
if let filespace = workspace.filespaces[url],
136+
let suggestion = filespace.presentingSuggestion
37137
{
38138
return .init(
39139
code: suggestion.text,
40-
language: await filespace.language,
140+
language: filespace.language,
41141
startLineIndex: suggestion.position.line,
42-
suggestionCount: await filespace.suggestions.count,
43-
currentSuggestionIndex: await filespace.suggestionIndex,
142+
suggestionCount: filespace.suggestions.count,
143+
currentSuggestionIndex: filespace.suggestionIndex,
44144
onSelectPreviousSuggestionTapped: {
45145
Task { @ServiceActor in
46146
let handler = PseudoCommandHandler()
@@ -73,49 +173,20 @@ extension WidgetDataSource: SuggestionWidgetDataSource {
73173

74174
func chatForFile(at url: URL) async -> ChatProvider? {
75175
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
76-
let buildChatProvider = { (service: ChatService) in
77-
ChatProvider(
78-
service: service,
79-
fileURL: url,
80-
onCloseChat: { [weak self] in
81-
if UserDefaults.shared.value(for: \.useGlobalChat) {
82-
self?.globalChat = nil
83-
self?.globalChatProvider = nil
84-
} else {
85-
self?.chats[url] = nil
86-
self?.chatProviders[url] = nil
87-
}
88-
let presenter = PresentInWindowSuggestionPresenter()
89-
presenter.closeChatRoom(fileURL: url)
90-
},
91-
onSwitchContext: { [weak self] in
92-
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
93-
UserDefaults.shared.set(!useGlobalChat, for: \.useGlobalChat)
94-
self?.createChatIfNeeded(for: url)
95-
let presenter = PresentInWindowSuggestionPresenter()
96-
presenter.presentChatRoom(fileURL: url)
97-
}
98-
)
99-
}
100-
101176
if useGlobalChat {
102-
if let globalChatProvider {
103-
return globalChatProvider
104-
} else if let globalChat {
105-
let new = buildChatProvider(globalChat)
106-
self.globalChatProvider = new
107-
return new
177+
if let globalChat {
178+
return globalChat.provider
108179
}
109180
} else {
110-
if let provider = chatProviders[url] {
111-
return provider
112-
} else if let service = chats[url] {
113-
let new = buildChatProvider(service)
114-
self.chatProviders[url] = new
115-
return new
181+
if let chat = chats[url] {
182+
return chat.provider
116183
}
117184
}
118185

119186
return nil
120187
}
188+
189+
func promptToCodeForFile(at url: URL) async -> PromptToCodeProvider? {
190+
return promptToCodes[url]?.provider
191+
}
121192
}

Core/Sources/Service/ScheduledCleaner.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ public final class ScheduledCleaner {
1414
let filespaces = workspace.filespaces
1515
for (url, filespace) in filespaces {
1616
if filespace.isExpired {
17-
WidgetDataSource.shared.chats[url] = nil
18-
WidgetDataSource.shared.chatProviders[url] = nil
17+
WidgetDataSource.shared.cleanup(for: url)
1918
}
2019
}
2120
// cleanup workspace

Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,18 @@ struct PresentInWindowSuggestionPresenter {
5454
controller.presentChatRoom(fileURL: fileURL)
5555
}
5656
}
57+
58+
func presentPromptToCode(fileURL: URL) {
59+
Task { @MainActor in
60+
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
61+
controller.presentPromptToCode(fileURL: fileURL)
62+
}
63+
}
64+
65+
func closePromptToCode(fileURL: URL) {
66+
Task { @MainActor in
67+
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
68+
controller.discardPromptToCode(fileURL: fileURL)
69+
}
70+
}
5771
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
public final class PromptToCodeProvider: ObservableObject {
5+
let id = UUID()
6+
7+
@Published public var code: String
8+
@Published public var language: String
9+
@Published public var description: String
10+
@Published public var isResponding: Bool
11+
@Published public var startLineIndex: Int
12+
@Published public var requirement: String
13+
@Published public var errorMessage: String
14+
@Published public var canRevert: Bool
15+
16+
public var onRevertTapped: () -> Void
17+
public var onStopRespondingTap: () -> Void
18+
public var onCancelTapped: () -> Void
19+
public var onAcceptSuggestionTapped: () -> Void
20+
public var onRequirementSent: (String) -> Void
21+
22+
public init(
23+
code: String = "",
24+
language: String = "",
25+
description: String = "",
26+
isResponding: Bool = false,
27+
startLineIndex: Int = 0,
28+
requirement: String = "",
29+
errorMessage: String = "",
30+
canRevert: Bool = false,
31+
onRevertTapped: @escaping () -> Void = {},
32+
onStopRespondingTap: @escaping () -> Void = {},
33+
onCancelTapped: @escaping () -> Void = {},
34+
onAcceptSuggestionTapped: @escaping () -> Void = {},
35+
onRequirementSent: @escaping (String) -> Void = { _ in }
36+
) {
37+
self.code = code
38+
self.language = language
39+
self.description = description
40+
self.isResponding = isResponding
41+
self.startLineIndex = startLineIndex
42+
self.requirement = requirement
43+
self.errorMessage = errorMessage
44+
self.canRevert = canRevert
45+
self.onRevertTapped = onRevertTapped
46+
self.onStopRespondingTap = onStopRespondingTap
47+
self.onCancelTapped = onCancelTapped
48+
self.onAcceptSuggestionTapped = onAcceptSuggestionTapped
49+
self.onRequirementSent = onRequirementSent
50+
}
51+
52+
func revert() {
53+
onRevertTapped()
54+
errorMessage = ""
55+
}
56+
func stopResponding() {
57+
onStopRespondingTap()
58+
errorMessage = ""
59+
}
60+
func cancel() { onCancelTapped() }
61+
func sendRequirement() {
62+
guard !isResponding else { return }
63+
guard !requirement.isEmpty else { return }
64+
onRequirementSent(requirement)
65+
requirement = ""
66+
errorMessage = ""
67+
}
68+
69+
func acceptSuggestion() { onAcceptSuggestionTapped() }
70+
}

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ struct ChatPanelInputArea: View {
241241
Image(systemName: "paperplane.fill")
242242
.padding(8)
243243
}
244-
.buttonStyle(.plain)
244+
.buttonStyle(.plain)
245245
.disabled(chat.isReceivingMessage)
246246
}
247247
.frame(maxWidth: .infinity)

0 commit comments

Comments
 (0)