Skip to content

Commit 1e293de

Browse files
committed
Add the explain code command
1 parent 80ca5d6 commit 1e293de

21 files changed

+317
-46
lines changed

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4949
C8EE079D29CC21300043B6D9 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE079C29CC21300043B6D9 /* AccountView.swift */; };
5050
C8EE079F29CC25C20043B6D9 /* OpenAIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE079E29CC25C20043B6D9 /* OpenAIView.swift */; };
51+
C8EE07A129CC9ED30043B6D9 /* ExplainSelectionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */; };
5152
/* End PBXBuildFile section */
5253

5354
/* Begin PBXContainerItemProxy section */
@@ -192,6 +193,7 @@
192193
C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = "<group>"; };
193194
C8EE079C29CC21300043B6D9 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
194195
C8EE079E29CC25C20043B6D9 /* OpenAIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIView.swift; sourceTree = "<group>"; };
196+
C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplainSelectionCommand.swift; sourceTree = "<group>"; };
195197
/* End PBXFileReference section */
196198

197199
/* Begin PBXFrameworksBuildPhase section */
@@ -254,6 +256,7 @@
254256
C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */,
255257
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
256258
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
259+
C8EE07A029CC9ED30043B6D9 /* ExplainSelectionCommand.swift */,
257260
C81458972939EFDC00135263 /* Info.plist */,
258261
C81458982939EFDC00135263 /* EditorExtension.entitlements */,
259262
);
@@ -523,6 +526,7 @@
523526
C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */,
524527
C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */,
525528
C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */,
529+
C8EE07A129CC9ED30043B6D9 /* ExplainSelectionCommand.swift in Sources */,
526530
C8009C032941C576007AA7E8 /* RealtimeSuggestionCommand.swift in Sources */,
527531
C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */,
528532
C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */,

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ let package = Package(
7373
dependencies: [
7474
"CopilotModel",
7575
"CopilotService",
76+
"OpenAIService",
7677
"Preferences",
7778
"XPCShared",
7879
"CGEventObserver",

Core/Sources/Client/AsyncXPCService.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@ public struct AsyncXPCService {
170170
}
171171
}
172172
}
173+
174+
public func explainSelection(editorContent: EditorContent) async throws -> UpdatedContent? {
175+
try await suggestionRequest(
176+
connection,
177+
editorContent,
178+
{ $0.explainSelection }
179+
)
180+
}
173181
}
174182

175183
struct AutoFinishContinuation<T> {

Core/Sources/OpenAIService/ChatGPTService.swift

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AsyncAlgorithms
22
import Foundation
3+
import Logger
34

45
public protocol ChatGPTServiceType {
56
func send(content: String, summary: String?) async throws -> AsyncThrowingStream<String, Error>
@@ -11,7 +12,7 @@ public protocol ChatGPTServiceType {
1112
public enum ChatGPTServiceError: Error, LocalizedError {
1213
case endpointIncorrect
1314
case responseInvalid
14-
15+
1516
public var errorDescription: String? {
1617
switch self {
1718
case .endpointIncorrect:
@@ -49,7 +50,7 @@ public struct ChatGPTError: Error, Codable, LocalizedError {
4950

5051
public actor ChatGPTService: ChatGPTServiceType, ObservableObject {
5152
public var temperature: Double
52-
public var model: ChatGPTModel
53+
public var model: String
5354
public var endpoint: String
5455
public var apiKey: String
5556
public var systemPrompt: String
@@ -62,20 +63,24 @@ public actor ChatGPTService: ChatGPTServiceType, ObservableObject {
6263
var cancelTask: Cancellable?
6364
var buildCompletionStreamAPI: CompletionStreamAPIBuilder = OpenAICompletionStreamAPI.init
6465

66+
deinit {
67+
print("deinit")
68+
}
69+
6570
public init(
6671
systemPrompt: String,
6772
apiKey: String,
68-
endpoint: String = "https://api.openai.com/v1/chat/completions",
69-
model: ChatGPTModel = .gpt_3_5_turbo,
73+
endpoint: String? = nil,
74+
model: String? = nil,
7075
temperature: Double = 1,
7176
maxToken: Int = 2048
7277
) {
7378
self.systemPrompt = systemPrompt
7479
self.apiKey = apiKey
75-
self.model = model
80+
self.model = model ?? "gpt-3.5-turbo"
7681
self.temperature = temperature
7782
self.maxToken = maxToken
78-
self.endpoint = endpoint
83+
self.endpoint = endpoint ?? "https://api.openai.com/v1/chat/completions"
7984
}
8085

8186
public func send(
@@ -88,23 +93,23 @@ public actor ChatGPTService: ChatGPTServiceType, ObservableObject {
8893
history.append(newMessage)
8994

9095
let requestBody = CompletionRequestBody(
91-
model: model.rawValue,
96+
model: model,
9297
messages: combineHistoryWithSystemPrompt(),
9398
temperature: temperature,
9499
stream: true,
95100
max_tokens: maxToken
96101
)
97102

98103
isReceivingMessage = true
99-
104+
100105
do {
101106
let api = buildCompletionStreamAPI(apiKey, url, requestBody)
102-
let (trunks, cancel) = try await api()
103-
cancelTask = cancel
104107

105108
return AsyncThrowingStream<String, Error> { continuation in
106109
Task {
107110
do {
111+
let (trunks, cancel) = try await api()
112+
cancelTask = cancel
108113
for try await trunk in trunks {
109114
guard let delta = trunk.choices.first?.delta else { continue }
110115

@@ -117,9 +122,9 @@ public actor ChatGPTService: ChatGPTServiceType, ObservableObject {
117122
}
118123
} else {
119124
history.append(.init(
125+
id: trunk.id,
120126
role: delta.role ?? .assistant,
121-
content: delta.content ?? "",
122-
id: trunk.id
127+
content: delta.content ?? ""
123128
))
124129
}
125130

@@ -131,13 +136,16 @@ public actor ChatGPTService: ChatGPTServiceType, ObservableObject {
131136
continuation.finish()
132137
isReceivingMessage = false
133138
} catch {
139+
Logger.service.error(error)
140+
history.append(.init(
141+
role: .assistant,
142+
content: error.localizedDescription
143+
))
144+
isReceivingMessage = false
134145
continuation.finish(throwing: error)
135146
}
136147
}
137148
}
138-
} catch {
139-
isReceivingMessage = false
140-
throw error
141149
}
142150
}
143151

@@ -160,12 +168,16 @@ extension ChatGPTService {
160168
func changeBuildCompletionStreamAPI(_ builder: @escaping CompletionStreamAPIBuilder) {
161169
buildCompletionStreamAPI = builder
162170
}
163-
164-
func combineHistoryWithSystemPrompt() -> [ChatMessage] {
171+
172+
func combineHistoryWithSystemPrompt() -> [CompletionRequestBody.Message] {
165173
if history.count > 4 {
166174
return [.init(role: .system, content: systemPrompt)] +
167-
history[history.endIndex - 4..<history.endIndex]
175+
history[history.endIndex - 4..<history.endIndex].map {
176+
.init(role: $0.role, content: $0.content)
177+
}
178+
}
179+
return [.init(role: .system, content: systemPrompt)] + history.map {
180+
.init(role: $0.role, content: $0.content)
168181
}
169-
return [.init(role: .system, content: systemPrompt)] + history
170182
}
171183
}

Core/Sources/OpenAIService/CompletionStreamAPI.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ protocol CompletionStreamAPI {
1212

1313
/// https://platform.openai.com/docs/api-reference/chat/create
1414
struct CompletionRequestBody: Codable {
15+
struct Message: Codable {
16+
var role: ChatMessage.Role
17+
var content: String
18+
}
19+
1520
var model: String
16-
var messages: [ChatMessage]
21+
var messages: [Message]
1722
var temperature: Double?
1823
var top_p: Double?
1924
var n: Double?

Core/Sources/OpenAIService/Models.swift

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,17 @@ public struct ChatMessage: Equatable, Codable {
1717
public var role: Role
1818
public var content: String
1919
public var summary: String?
20-
public var id: String?
20+
public var id: String
2121

22-
public init(role: Role, content: String, summary: String? = nil, id: String? = nil) {
22+
public init(
23+
id: String = UUID().uuidString,
24+
role: Role,
25+
content: String,
26+
summary: String? = nil
27+
) {
2328
self.role = role
2429
self.content = content
2530
self.summary = summary
2631
self.id = id
2732
}
2833
}
29-
30-
public enum ChatGPTModel: String {
31-
case gpt_3_5_turbo = "gpt-3.5-turbo"
32-
case gpt_3_5_turbo_0301 = "gpt-3.5-turbo-0301"
33-
case gpt_4_0314 = "gpt-4-0314"
34-
case gpt_4_32k = "gpt-4-32k"
35-
case gpt_4_32k_0314 = "gpt-4-32k-0314"
36-
}

Core/Sources/Service/SuggestionCommandHandler/CommentBaseCommandHandler.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,16 @@ struct CommentBaseCommandHandler: SuggestionCommandHandler {
171171

172172
return nil
173173
}
174+
175+
func explainSelection(editor: EditorContent) async throws -> UpdatedContent? {
176+
throw NotSupportedInCommentMode()
177+
}
178+
}
179+
180+
// MARK: - Unsupported
181+
182+
extension CommentBaseCommandHandler {
183+
struct NotSupportedInCommentMode: Error, LocalizedError {
184+
var errorDescription: String { "This command is not supported in comment mode." }
185+
}
174186
}

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct PseudoCommandHandler {
1717
lines: [],
1818
uti: "",
1919
cursorPosition: .outOfScope,
20+
selections: [],
2021
tabSize: 0,
2122
indentSize: 0,
2223
usesTabsForIndentation: false
@@ -30,6 +31,7 @@ struct PseudoCommandHandler {
3031
lines: [],
3132
uti: "",
3233
cursorPosition: .outOfScope,
34+
selections: [],
3335
tabSize: 0,
3436
indentSize: 0,
3537
usesTabsForIndentation: false
@@ -60,6 +62,7 @@ struct PseudoCommandHandler {
6062
lines: [],
6163
uti: "",
6264
cursorPosition: .outOfScope,
65+
selections: [],
6366
tabSize: 0,
6467
indentSize: 0,
6568
usesTabsForIndentation: false
@@ -123,6 +126,7 @@ private extension PseudoCommandHandler {
123126
lines: content.lines,
124127
uti: uti,
125128
cursorPosition: content.cursorPosition,
129+
selections: [],
126130
tabSize: tabSize,
127131
indentSize: indentSize,
128132
usesTabsForIndentation: usesTabsForIndentation

Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ protocol SuggestionCommandHandler {
1616
func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
1717
@ServiceActor
1818
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
19+
@ServiceActor
20+
func explainSelection(editor: EditorContent) async throws -> UpdatedContent?
1921
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import CopilotService
33
import Environment
44
import Foundation
55
import Logger
6+
import OpenAIService
67
import SuggestionInjector
8+
import SuggestionWidget
79
import XPCShared
810

911
@ServiceActor
@@ -173,4 +175,42 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
173175
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? {
174176
return try await presentSuggestions(editor: editor)
175177
}
178+
179+
func explainSelection(editor: EditorContent) async throws -> UpdatedContent? {
180+
Task {
181+
do {
182+
try await _explainSelection(editor: editor)
183+
} catch {
184+
presenter.presentError(error)
185+
}
186+
}
187+
return nil
188+
}
189+
190+
private func _explainSelection(editor: EditorContent) async throws {
191+
presenter.markAsProcessing(true)
192+
defer { presenter.markAsProcessing(false) }
193+
194+
let fileURL = try await Environment.fetchCurrentFileURL()
195+
let endpoint = UserDefaults.shared.value(for: \.chatGPTEndpoint)
196+
let model = UserDefaults.shared.value(for: \.chatGPTModel)
197+
guard let selection = editor.selections.last else { return }
198+
let service = ChatGPTService(
199+
systemPrompt: """
200+
You are a code explanation engine, you can only explain the code, do not interpret or translate it. Reply in Chinese
201+
""",
202+
apiKey: UserDefaults.shared.value(for: \.openAIAPIKey),
203+
endpoint: endpoint.isEmpty ? nil : endpoint,
204+
model: model.isEmpty ? nil : model,
205+
temperature: 1,
206+
maxToken: 2048
207+
)
208+
209+
let code = editor.selectedCode(in: selection)
210+
Task {
211+
try? await service.send(content: code, summary: "Explain selected code.")
212+
}
213+
214+
presenter.presentChatGPTConversation(service, fileURL: fileURL)
215+
}
176216
}

0 commit comments

Comments
 (0)