Skip to content

Commit b5c765a

Browse files
committed
Add command chat with selection
1 parent f1d1ac3 commit b5c765a

File tree

9 files changed

+134
-7
lines changed

9 files changed

+134
-7
lines changed

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
C882175A294187E100A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C8821759294187E100A22FD3 /* Client */; };
4747
C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; };
4848
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
49+
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; };
4950
C8EE079D29CC21300043B6D9 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE079C29CC21300043B6D9 /* AccountView.swift */; };
5051
C8EE079F29CC25C20043B6D9 /* OpenAIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE079E29CC25C20043B6D9 /* OpenAIView.swift */; };
5152
C8EE07A129CC9ED30043B6D9 /* ExplainSelectionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */; };
@@ -191,6 +192,7 @@
191192
C87F3E61293DD004008523E8 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
192193
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = "<group>"; };
193194
C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = "<group>"; };
195+
C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWithSelection.swift; sourceTree = "<group>"; };
194196
C8EE079C29CC21300043B6D9 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
195197
C8EE079E29CC25C20043B6D9 /* OpenAIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIView.swift; sourceTree = "<group>"; };
196198
C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplainSelectionCommand.swift; sourceTree = "<group>"; };
@@ -257,6 +259,7 @@
257259
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
258260
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
259261
C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */,
262+
C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */,
260263
C81458972939EFDC00135263 /* Info.plist */,
261264
C81458982939EFDC00135263 /* EditorExtension.entitlements */,
262265
);
@@ -519,6 +522,7 @@
519522
isa = PBXSourcesBuildPhase;
520523
buildActionMask = 2147483647;
521524
files = (
525+
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */,
522526
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */,
523527
C8520301293C4D9000460097 /* Helpers.swift in Sources */,
524528
C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */,

Core/Sources/Client/AsyncXPCService.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ public struct AsyncXPCService {
178178
{ $0.explainSelection }
179179
)
180180
}
181+
182+
public func chatWithSelection(editorContent: EditorContent) async throws -> UpdatedContent? {
183+
try await suggestionRequest(
184+
connection,
185+
editorContent,
186+
{ $0.chatWithSelection }
187+
)
188+
}
181189
}
182190

183191
struct AutoFinishContinuation<T> {

Core/Sources/Service/SuggestionCommandHandler/CommentBaseCommandHandler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
175175
func explainSelection(editor: EditorContent) async throws -> UpdatedContent? {
176176
throw NotSupportedInCommentMode()
177177
}
178+
179+
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
180+
throw NotSupportedInCommentMode()
181+
}
178182
}
179183

180184
// MARK: - Unsupported

Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ protocol SuggestionCommandHandler {
1818
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
1919
@ServiceActor
2020
func explainSelection(editor: EditorContent) async throws -> UpdatedContent?
21+
@ServiceActor
22+
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent?
2123
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,14 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
194194
let fileURL = try await Environment.fetchCurrentFileURL()
195195
let endpoint = UserDefaults.shared.value(for: \.chatGPTEndpoint)
196196
let model = UserDefaults.shared.value(for: \.chatGPTModel)
197-
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
197+
var language = UserDefaults.shared.value(for: \.chatGPTLanguage)
198+
if language.isEmpty { language = "English" }
198199
guard let selection = editor.selections.last else { return }
199200

200201
let service = ChatGPTService(
201202
systemPrompt: """
202-
You are a code explanation engine, you can only explain the code concisely, do not interpret or translate it. Reply in \(
203-
language
204-
.isEmpty ? language : "English"
205-
)
203+
You are a code explanation engine, you can only explain the code concisely, do not interpret or translate it
204+
Reply in \(language)
206205
""",
207206
apiKey: UserDefaults.shared.value(for: \.openAIAPIKey),
208207
endpoint: endpoint.isEmpty ? nil : endpoint,
@@ -214,15 +213,82 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
214213
let code = editor.selectedCode(in: selection)
215214
Task {
216215
try? await service.send(
217-
content: removeContinousSpaces(from: code),
216+
content: removeContinuousSpaces(from: code),
218217
summary: "Explain selected code from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character + 1)`."
219218
)
220219
}
221220

222221
presenter.presentChatGPTConversation(service, fileURL: fileURL)
223222
}
223+
224+
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
225+
Task {
226+
do {
227+
try await _chatWithSelection(editor: editor)
228+
} catch {
229+
presenter.presentError(error)
230+
}
231+
}
232+
return nil
233+
}
234+
235+
private func _chatWithSelection(editor: EditorContent) async throws {
236+
presenter.markAsProcessing(true)
237+
defer { presenter.markAsProcessing(false) }
238+
239+
let fileURL = try await Environment.fetchCurrentFileURL()
240+
let endpoint = UserDefaults.shared.value(for: \.chatGPTEndpoint)
241+
let model = UserDefaults.shared.value(for: \.chatGPTModel)
242+
var language = UserDefaults.shared.value(for: \.chatGPTLanguage)
243+
if language.isEmpty { language = "English" }
244+
245+
let code = {
246+
guard let selection = editor.selections.last,
247+
selection.start != selection.end else { return "" }
248+
return editor.selectedCode(in: selection)
249+
}()
250+
251+
let prompt = {
252+
if code.isEmpty {
253+
return """
254+
You are a senior programmer, you will answer my questions concisely in \(language)
255+
"""
256+
}
257+
return """
258+
You are a senior programmer, you will answer my questions concisely in \(
259+
language
260+
) about the code
261+
```
262+
\(removeContinuousSpaces(from: code))
263+
```
264+
"""
265+
}()
266+
267+
let service = ChatGPTService(
268+
systemPrompt: prompt,
269+
apiKey: UserDefaults.shared.value(for: \.openAIAPIKey),
270+
endpoint: endpoint.isEmpty ? nil : endpoint,
271+
model: model.isEmpty ? nil : model,
272+
temperature: 1,
273+
maxToken: UserDefaults.shared.value(for: \.chatGPTMaxToken)
274+
)
275+
276+
Task {
277+
if !code.isEmpty, let selection = editor.selections.last {
278+
await service.mutateHistory { history in
279+
history.append(.init(
280+
role: .user,
281+
content: "",
282+
summary: "Chat about selected code from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character)`.\nThe code will persisted in the conversation."
283+
))
284+
}
285+
}
286+
}
287+
288+
presenter.presentChatGPTConversation(service, fileURL: fileURL)
289+
}
224290
}
225291

226-
func removeContinousSpaces(from string: String) -> String {
292+
func removeContinuousSpaces(from string: String) -> String {
227293
return string.replacingOccurrences(of: " +", with: " ", options: .regularExpression)
228294
}

Core/Sources/Service/XPCService.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,15 @@ public class XPCService: NSObject, XPCServiceProtocol {
223223
try await handler.explainSelection(editor: editor)
224224
}
225225
}
226+
227+
public func chatWithSelection(
228+
editorContent: Data,
229+
withReply reply: @escaping (Data?, Error?) -> Void
230+
) {
231+
replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in
232+
try await handler.chatWithSelection(editor: editor)
233+
}
234+
}
226235

227236
// MARK: - Settings
228237

Core/Sources/XPCShared/XPCServiceProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public protocol XPCServiceProtocol {
4040
editorContent: Data,
4141
withReply reply: @escaping (Data?, Error?) -> Void
4242
)
43+
func chatWithSelection(
44+
editorContent: Data,
45+
withReply reply: @escaping (Data?, Error?) -> Void
46+
)
4347

4448
func toggleRealtimeSuggestion(withReply reply: @escaping (Error?) -> Void)
4549

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Client
2+
import CopilotModel
3+
import Foundation
4+
import XcodeKit
5+
6+
class ChatWithSelectionCommand: NSObject, XCSourceEditorCommand, CommandType {
7+
var name: String { "Chat With Selection" }
8+
9+
func perform(
10+
with invocation: XCSourceEditorCommandInvocation,
11+
completionHandler: @escaping (Error?) -> Void
12+
) {
13+
Task {
14+
do {
15+
let service = try getService()
16+
if let content = try await service.chatWithSelection(
17+
editorContent: .init(invocation)
18+
) {
19+
invocation.accept(content)
20+
}
21+
completionHandler(nil)
22+
} catch is CancellationError {
23+
completionHandler(nil)
24+
} catch {
25+
completionHandler(error)
26+
}
27+
}
28+
}
29+
}

EditorExtension/SourceEditorExtension.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension {
1313
ToggleRealtimeSuggestionsCommand(),
1414
RealtimeSuggestionsCommand(),
1515
PrefetchSuggestionsCommand(),
16+
ChatWithSelectionCommand(),
1617
ExplainSelectionCommand(),
1718
].map(makeCommandDefinition)
1819
}

0 commit comments

Comments
 (0)