Skip to content

Commit 2e13528

Browse files
committed
Add prompt to code service
1 parent aacb4b1 commit 2e13528

File tree

4 files changed

+302
-0
lines changed

4 files changed

+302
-0
lines changed

Core/Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ let package = Package(
8787
"AXExtension",
8888
"Logger",
8989
"ChatService",
90+
"PromptToCodeService",
9091
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
9192
]
9293
),
@@ -154,5 +155,10 @@ let package = Package(
154155
.target(name: "ChatPlugins", dependencies: ["OpenAIService", "Environment", "Terminal"]),
155156
.target(name: "Terminal"),
156157
.target(name: "ChatService", dependencies: ["OpenAIService", "ChatPlugins", "Environment"]),
158+
.target(
159+
name: "PromptToCodeService",
160+
dependencies: ["OpenAIService", "Environment", "CopilotService", "CopilotModel"]
161+
),
162+
.testTarget(name: "PromptToCodeServiceTests", dependencies: ["PromptToCodeService"]),
157163
]
158164
)
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import CopilotModel
2+
import CopilotService
3+
import Foundation
4+
import OpenAIService
5+
6+
public final class PromptToCodeService: ObservableObject {
7+
var designatedPromptToCodeAPI: PromptToCodeAPI?
8+
var promptToCodeAPI: PromptToCodeAPI {
9+
designatedPromptToCodeAPI ?? OpenAIPromptToCodeAPI()
10+
}
11+
var runningAPI: PromptToCodeAPI?
12+
13+
@Published public var oldCode: String?
14+
@Published public var code: String
15+
@Published public var isResponding: Bool = false
16+
@Published public var description: String = ""
17+
public var canRevert: Bool { oldCode != nil }
18+
public var selectionRange: CursorRange
19+
public var language: CopilotLanguage
20+
public var indentSize: Int
21+
public var usesTabsForIndentation: Bool
22+
23+
public init(
24+
code: String,
25+
selectionRange: CursorRange,
26+
language: CopilotLanguage,
27+
identSize: Int,
28+
usesTabsForIndentation: Bool
29+
) {
30+
self.code = code
31+
self.selectionRange = selectionRange
32+
self.language = language
33+
indentSize = identSize
34+
self.usesTabsForIndentation = usesTabsForIndentation
35+
}
36+
37+
public func modifyCode(prompt: String) async throws {
38+
let api = promptToCodeAPI
39+
runningAPI = api
40+
isResponding = true
41+
let toBemodified = code
42+
oldCode = code
43+
code = ""
44+
description = ""
45+
defer { isResponding = false }
46+
let stream = try await api.modifyCode(
47+
code: toBemodified,
48+
language: language,
49+
indentSize: indentSize,
50+
usesTabsForIndentation: usesTabsForIndentation,
51+
requirement: prompt
52+
)
53+
for try await fragment in stream {
54+
Task { @MainActor in
55+
code = fragment.code
56+
description = fragment.description
57+
}
58+
}
59+
}
60+
61+
public func revert() {
62+
guard let oldCode = oldCode else { return }
63+
code = oldCode
64+
self.oldCode = nil
65+
}
66+
67+
public func generateCompletion() -> CopilotCompletion {
68+
.init(
69+
text: code,
70+
position: selectionRange.start,
71+
uuid: UUID().uuidString,
72+
range: selectionRange,
73+
displayText: code
74+
)
75+
}
76+
77+
public func stopResponding() {
78+
runningAPI?.stopResponding()
79+
isResponding = false
80+
}
81+
}
82+
83+
protocol PromptToCodeAPI {
84+
func modifyCode(
85+
code: String,
86+
language: CopilotLanguage,
87+
indentSize: Int,
88+
usesTabsForIndentation: Bool,
89+
requirement: String
90+
) async throws -> AsyncThrowingStream<(code: String, description: String), Error>
91+
92+
func stopResponding()
93+
}
94+
95+
final class OpenAIPromptToCodeAPI: PromptToCodeAPI {
96+
var service: (any ChatGPTServiceType)?
97+
98+
func stopResponding() {
99+
Task {
100+
await service?.stopReceivingMessage()
101+
}
102+
}
103+
104+
func modifyCode(
105+
code: String,
106+
language: CopilotLanguage,
107+
indentSize: Int,
108+
usesTabsForIndentation: Bool,
109+
requirement: String
110+
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
111+
let prompt = {
112+
let indentRule = usesTabsForIndentation ? "\(indentSize) tabs" : "\(indentSize) spaces"
113+
if code.isEmpty {
114+
return """
115+
You are a senior programer in writing code in \(language.rawValue).
116+
117+
Please write a piece of code that meets my requirements. The indentation should be \(
118+
indentRule
119+
).
120+
121+
Please reply to me start with the code block, followed by a short description in 1-3 sentences about what you did.
122+
"""
123+
} else {
124+
return """
125+
You are a senior programer in writing code in \(language.rawValue).
126+
127+
Please mutate the following code with my requirements. The indentation should be \(
128+
indentRule
129+
).
130+
131+
Please reply to me start with the code block followed by a short description about what you did in 1-3 sentences.
132+
133+
```
134+
\(code)
135+
```
136+
"""
137+
}
138+
}()
139+
140+
let chatGPTService = ChatGPTService(systemPrompt: prompt)
141+
service = chatGPTService
142+
let stream = try await chatGPTService.send(content: requirement)
143+
return .init { continuation in
144+
Task {
145+
var content = ""
146+
do {
147+
for try await fragment in stream {
148+
content.append(fragment)
149+
continuation.yield(extractCodeAndDescription(from: content))
150+
}
151+
continuation.finish()
152+
} catch {
153+
continuation.finish(throwing: error)
154+
}
155+
}
156+
}
157+
}
158+
159+
func extractCodeAndDescription(from content: String) -> (code: String, description: String) {
160+
func extractCodeFromMarkdown(_ markdown: String) -> (code: String, endIndex: Int)? {
161+
let codeBlockRegex = try! NSRegularExpression(
162+
pattern: #"```(?:\w+)?[\n]([\s\S]+?)[\n]```"#,
163+
options: .dotMatchesLineSeparators
164+
)
165+
let range = NSRange(markdown.startIndex..<markdown.endIndex, in: markdown)
166+
if let match = codeBlockRegex.firstMatch(in: markdown, options: [], range: range) {
167+
let codeBlockRange = Range(match.range(at: 1), in: markdown)!
168+
return (String(markdown[codeBlockRange]), match.range(at: 0).upperBound)
169+
}
170+
171+
let incompleteCodeBlockRegex = try! NSRegularExpression(
172+
pattern: #"```(?:\w+)?[\n]([\s\S]+?)$"#,
173+
options: .dotMatchesLineSeparators
174+
)
175+
let range2 = NSRange(markdown.startIndex..<markdown.endIndex, in: markdown)
176+
if let match = incompleteCodeBlockRegex.firstMatch(
177+
in: markdown,
178+
options: [],
179+
range: range2
180+
) {
181+
let codeBlockRange = Range(match.range(at: 1), in: markdown)!
182+
return (String(markdown[codeBlockRange]), match.range(at: 0).upperBound)
183+
}
184+
return nil
185+
}
186+
187+
guard let (code, endIndex) = extractCodeFromMarkdown(content) else {
188+
return ("", "")
189+
}
190+
191+
func extractDescriptionFromMarkdown(_ markdown: String, startIndex: Int) -> String {
192+
let startIndex = markdown.index(markdown.startIndex, offsetBy: startIndex)
193+
guard startIndex < markdown.endIndex else { return "" }
194+
let range = startIndex..<markdown.endIndex
195+
let description = String(markdown[range])
196+
.trimmingCharacters(in: .whitespacesAndNewlines)
197+
return description
198+
}
199+
200+
let description = extractDescriptionFromMarkdown(content, startIndex: endIndex)
201+
202+
return (code, description)
203+
}
204+
}
205+
206+
final class CopilotPromptToCodeAPI: PromptToCodeAPI {
207+
func stopResponding() {
208+
fatalError()
209+
}
210+
211+
func modifyCode(
212+
code: String,
213+
language: CopilotLanguage,
214+
indentSize: Int,
215+
usesTabsForIndentation: Bool,
216+
requirement: String
217+
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
218+
fatalError()
219+
}
220+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import XCTest
2+
@testable import PromptToCodeService
3+
4+
final class ExtractCodeFromChatGPTTests: XCTestCase {
5+
func test_extract_from_no_code_block() {
6+
let api = OpenAIPromptToCodeAPI()
7+
let result = api.extractCodeAndDescription(from: """
8+
hello world!
9+
""")
10+
11+
XCTAssertEqual(result.code, "")
12+
XCTAssertEqual(result.description, "")
13+
}
14+
15+
func test_extract_from_incomplete_code_block() {
16+
let api = OpenAIPromptToCodeAPI()
17+
let result = api.extractCodeAndDescription(from: """
18+
```swift
19+
func foo() {}
20+
""")
21+
22+
XCTAssertEqual(result.code, "func foo() {}")
23+
XCTAssertEqual(result.description, "")
24+
}
25+
26+
func test_extract_from_complete_code_block() {
27+
let api = OpenAIPromptToCodeAPI()
28+
let result = api.extractCodeAndDescription(from: """
29+
```swift
30+
func foo() {}
31+
32+
func bar() {}
33+
```
34+
35+
Description
36+
""")
37+
38+
XCTAssertEqual(result.code, "func foo() {}\n\nfunc bar() {}")
39+
XCTAssertEqual(result.description, "Description")
40+
}
41+
42+
func test_extract_from_incomplete_code_block_without_language() {
43+
let api = OpenAIPromptToCodeAPI()
44+
let result = api.extractCodeAndDescription(from: """
45+
```
46+
func foo() {}
47+
""")
48+
49+
XCTAssertEqual(result.code, "func foo() {}")
50+
XCTAssertEqual(result.description, "")
51+
}
52+
53+
func test_extract_from_code_block_without_language() {
54+
let api = OpenAIPromptToCodeAPI()
55+
let result = api.extractCodeAndDescription(from: """
56+
```
57+
func foo() {}
58+
59+
func bar() {}
60+
```
61+
62+
Description
63+
""")
64+
65+
XCTAssertEqual(result.code, "func foo() {}\n\nfunc bar() {}")
66+
XCTAssertEqual(result.description, "Description")
67+
}
68+
69+
}

TestPlan.xctestplan

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@
6363
"identifier" : "SuggestionWidgetTests",
6464
"name" : "SuggestionWidgetTests"
6565
}
66+
},
67+
{
68+
"target" : {
69+
"containerPath" : "container:Core",
70+
"identifier" : "PromptToCodeServiceTests",
71+
"name" : "PromptToCodeServiceTests"
72+
}
6673
}
6774
],
6875
"version" : 1

0 commit comments

Comments
 (0)