Skip to content

Commit 12c6548

Browse files
committed
Merge branch 'feature/command-chat-with-selection' into develop
2 parents f1d1ac3 + d8c908b commit 12c6548

File tree

14 files changed

+270
-42
lines changed

14 files changed

+270
-42
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 */,

Copilot for Xcode/OpenAIView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ struct OpenAIView: View {
7070
Text("Reply in Language")
7171
TextField(
7272
text: $settings.chatGPTLanguage,
73-
prompt: Text("English")
73+
prompt: Text("e.g. English. Leave it blank to let the bot decide.")
7474
) {
7575
EmptyView()
7676
}.textFieldStyle(.copilot)

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/OpenAIService/ChatGPTService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ extension ChatGPTService {
188188
}
189189

190190
func combineHistoryWithSystemPrompt() -> [CompletionRequestBody.Message] {
191-
if history.count > 4 {
191+
if history.count > 5 {
192192
return [.init(role: .system, content: systemPrompt)] +
193-
history[history.endIndex - 4..<history.endIndex].map {
193+
history[history.endIndex - 5..<history.endIndex].map {
194194
.init(role: $0.role, content: $0.content)
195195
}
196196
}

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: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
199199

200200
let service = ChatGPTService(
201201
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-
)
202+
You are a code explanation engine, you can only explain the code concisely, do not interpret or translate it
203+
Reply in \(language.isEmpty ? "" : "in \(language)")
206204
""",
207205
apiKey: UserDefaults.shared.value(for: \.openAIAPIKey),
208206
endpoint: endpoint.isEmpty ? nil : endpoint,
@@ -214,15 +212,83 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
214212
let code = editor.selectedCode(in: selection)
215213
Task {
216214
try? await service.send(
217-
content: removeContinousSpaces(from: code),
215+
content: removeContinuousSpaces(from: code),
218216
summary: "Explain selected code from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character + 1)`."
219217
)
220218
}
221219

222220
presenter.presentChatGPTConversation(service, fileURL: fileURL)
223221
}
222+
223+
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
224+
Task {
225+
do {
226+
try await _chatWithSelection(editor: editor)
227+
} catch {
228+
presenter.presentError(error)
229+
}
230+
}
231+
return nil
232+
}
233+
234+
private func _chatWithSelection(editor: EditorContent) async throws {
235+
presenter.markAsProcessing(true)
236+
defer { presenter.markAsProcessing(false) }
237+
238+
let fileURL = try await Environment.fetchCurrentFileURL()
239+
let endpoint = UserDefaults.shared.value(for: \.chatGPTEndpoint)
240+
let model = UserDefaults.shared.value(for: \.chatGPTModel)
241+
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
242+
243+
let code = {
244+
guard let selection = editor.selections.last,
245+
selection.start != selection.end else { return "" }
246+
return editor.selectedCode(in: selection)
247+
}()
248+
249+
let prompt = {
250+
if code.isEmpty {
251+
return """
252+
You are a senior programmer, you will answer my questions concisely \(
253+
language.isEmpty ? "" : "in \(language)"
254+
)
255+
"""
256+
}
257+
return """
258+
You are a senior programmer, you will answer my questions concisely in \(
259+
language.isEmpty ? "" : "in \(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/Models.swift

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,7 @@ public struct EditorContent: Codable {
4242
public var usesTabsForIndentation: Bool
4343

4444
public func selectedCode(in selection: Selection) -> String {
45-
let startPosition = selection.start
46-
let endPosition = selection.end
47-
48-
guard startPosition.line >= 0, startPosition.line < lines.count else { return "" }
49-
guard endPosition.line >= 0, endPosition.line < lines.count else { return "" }
50-
guard endPosition.line >= startPosition.line else { return "" }
51-
52-
var code = ""
53-
if startPosition.line == endPosition.line {
54-
let line = lines[startPosition.line]
55-
let startIndex = line.index(line.startIndex, offsetBy: startPosition.character)
56-
let endIndex = line.index(line.startIndex, offsetBy: endPosition.character)
57-
code = String(line[startIndex...endIndex])
58-
} else {
59-
let startLine = lines[startPosition.line]
60-
let startIndex = startLine.index(
61-
startLine.startIndex,
62-
offsetBy: startPosition.character
63-
)
64-
code += String(startLine[startIndex...])
65-
66-
if startPosition.line + 1 < endPosition.line {
67-
for line in lines[startPosition.line + 1...endPosition.line - 1] {
68-
code += line
69-
}
70-
}
71-
72-
let endLine = lines[endPosition.line]
73-
let endIndex = endLine.index(endLine.startIndex, offsetBy: endPosition.character)
74-
code += String(endLine[...endIndex])
75-
}
76-
77-
return code
45+
return XPCShared.selectedCode(in: selection, for: lines)
7846
}
7947
}
8048

@@ -89,3 +57,47 @@ public struct UpdatedContent: Codable {
8957
public var newCursor: CursorPosition?
9058
public var modifications: [Modification]
9159
}
60+
61+
func selectedCode(in selection: EditorContent.Selection, for lines: [String]) -> String {
62+
let startPosition = selection.start
63+
var endPosition = CursorPosition(
64+
line: selection.end.line,
65+
character: selection.end.character - 1
66+
)
67+
68+
guard startPosition.line >= 0, startPosition.line < lines.count else { return "" }
69+
guard startPosition.character >= 0,
70+
startPosition.character < lines[startPosition.line].count else { return "" }
71+
guard endPosition.line >= 0, endPosition.line < lines.count else { return "" }
72+
guard endPosition.line >= startPosition.line else { return "" }
73+
guard endPosition.character >= 0,
74+
endPosition.character < lines[endPosition.line].count else { return "" }
75+
76+
var code = ""
77+
if startPosition.line == endPosition.line {
78+
guard endPosition.character >= startPosition.character else { return "" }
79+
let line = lines[startPosition.line]
80+
let startIndex = line.index(line.startIndex, offsetBy: startPosition.character)
81+
let endIndex = line.index(line.startIndex, offsetBy: endPosition.character)
82+
code = String(line[startIndex...endIndex])
83+
} else {
84+
let startLine = lines[startPosition.line]
85+
let startIndex = startLine.index(
86+
startLine.startIndex,
87+
offsetBy: startPosition.character
88+
)
89+
code += String(startLine[startIndex...])
90+
91+
if startPosition.line + 1 < endPosition.line {
92+
for line in lines[startPosition.line + 1...endPosition.line - 1] {
93+
code += line
94+
}
95+
}
96+
97+
let endLine = lines[endPosition.line]
98+
let endIndex = endLine.index(endLine.startIndex, offsetBy: endPosition.character)
99+
code += String(endLine[...endIndex])
100+
}
101+
102+
return code
103+
}

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

0 commit comments

Comments
 (0)