Skip to content

Commit 4543d5e

Browse files
committed
Implement CopilotPromptToCodeAPI for fun
1 parent 7d8609f commit 4543d5e

File tree

5 files changed

+101
-142
lines changed

5 files changed

+101
-142
lines changed

Core/Sources/PromptToCodeService/CopilotPromptToCodeAPI.swift

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,85 @@ import Foundation
44
import OpenAIService
55

66
final class CopilotPromptToCodeAPI: PromptToCodeAPI {
7+
var task: Task<Void, Never>?
8+
79
func stopResponding() {
8-
fatalError()
10+
task?.cancel()
911
}
1012

1113
func modifyCode(
1214
code: String,
1315
language: CopilotLanguage,
1416
indentSize: Int,
1517
usesTabsForIndentation: Bool,
16-
requirement: String
18+
requirement: String,
19+
projectRootURL: URL,
20+
fileURL: URL,
21+
allCode: String
1722
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
18-
fatalError()
23+
let copilotService = CopilotSuggestionService(projectRootURL: projectRootURL)
24+
let relativePath = {
25+
let filePath = fileURL.path
26+
let rootPath = projectRootURL.path
27+
if let range = filePath.range(of: rootPath),
28+
range.lowerBound == filePath.startIndex
29+
{
30+
let relativePath = filePath.replacingCharacters(
31+
in: filePath.startIndex..<range.upperBound,
32+
with: ""
33+
)
34+
return relativePath
35+
}
36+
return filePath
37+
}()
38+
39+
let comment = """
40+
// update the following code, \(requirement.split(separator: "\n").joined(separator: " ")).
41+
\(code.split(separator: "\n").map { "//\($0)" }.joined(separator: "\n"))
42+
43+
44+
45+
// Path: \(relativePath)
46+
47+
"""
48+
let lineCount = comment.breakLines().count
49+
50+
return .init { continuation in
51+
self.task = Task {
52+
do {
53+
let result = try await copilotService.getCompletions(
54+
fileURL: fileURL,
55+
content: comment,
56+
cursorPosition: .init(line: lineCount - 4, character: 0),
57+
tabSize: indentSize,
58+
indentSize: indentSize,
59+
usesTabsForIndentation: usesTabsForIndentation,
60+
ignoreSpaceOnlySuggestions: true
61+
)
62+
try Task.checkCancellation()
63+
guard let first = result.first else { throw CancellationError() }
64+
continuation.yield((first.text, ""))
65+
continuation.finish()
66+
} catch {
67+
continuation.finish(throwing: error)
68+
}
69+
}
70+
}
71+
}
72+
}
73+
74+
extension String {
75+
/// Break a string into lines.
76+
func breakLines() -> [String] {
77+
let lines = split(separator: "\n", omittingEmptySubsequences: false)
78+
var all = [String]()
79+
for (index, line) in lines.enumerated() {
80+
if index == lines.endIndex - 1 {
81+
all.append(String(line))
82+
} else {
83+
all.append(String(line) + "\n")
84+
}
85+
}
86+
return all
1987
}
2088
}

Core/Sources/PromptToCodeService/OpenAIPromptToCodeAPI.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ final class OpenAIPromptToCodeAPI: PromptToCodeAPI {
1717
language: CopilotLanguage,
1818
indentSize: Int,
1919
usesTabsForIndentation: Bool,
20-
requirement: String
20+
requirement: String,
21+
projectRootURL: URL,
22+
fileURL: URL,
23+
allCode: String
2124
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
2225
let userPreferredLanguage = UserDefaults.shared.value(for: \.chatGPTLanguage)
2326
let textLanguage = userPreferredLanguage.isEmpty ? "" : "in \(userPreferredLanguage)"

Core/Sources/PromptToCodeService/PromptToCodeService.swift

Lines changed: 14 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,29 @@ public final class PromptToCodeService: ObservableObject {
2222
public var language: CopilotLanguage
2323
public var indentSize: Int
2424
public var usesTabsForIndentation: Bool
25+
public var projectRootURL: URL
26+
public var fileURL: URL
27+
public var allCode: String
2528

2629
public init(
2730
code: String,
2831
selectionRange: CursorRange,
2932
language: CopilotLanguage,
3033
identSize: Int,
3134
usesTabsForIndentation: Bool
35+
usesTabsForIndentation: Bool,
36+
projectRootURL: URL,
37+
fileURL: URL,
38+
allCode: String
3239
) {
3340
self.code = code
3441
self.selectionRange = selectionRange
3542
self.language = language
3643
indentSize = identSize
3744
self.usesTabsForIndentation = usesTabsForIndentation
45+
self.projectRootURL = projectRootURL
46+
self.fileURL = fileURL
47+
self.allCode = allCode
3848
}
3949

4050
public func modifyCode(prompt: String) async throws {
@@ -112,142 +122,11 @@ protocol PromptToCodeAPI {
112122
language: CopilotLanguage,
113123
indentSize: Int,
114124
usesTabsForIndentation: Bool,
115-
requirement: String
125+
requirement: String,
126+
projectRootURL: URL,
127+
fileURL: URL,
128+
allCode: String
116129
) async throws -> AsyncThrowingStream<(code: String, description: String), Error>
117130

118131
func stopResponding()
119132
}
120-
121-
final class OpenAIPromptToCodeAPI: PromptToCodeAPI {
122-
var service: (any ChatGPTServiceType)?
123-
124-
func stopResponding() {
125-
Task {
126-
await service?.stopReceivingMessage()
127-
}
128-
}
129-
130-
func modifyCode(
131-
code: String,
132-
language: CopilotLanguage,
133-
indentSize: Int,
134-
usesTabsForIndentation: Bool,
135-
requirement: String
136-
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
137-
let userPreferredLanguage = UserDefaults.shared.value(for: \.chatGPTLanguage)
138-
let textLanguage = userPreferredLanguage.isEmpty ? "" : "in \(userPreferredLanguage)"
139-
140-
let prompt = {
141-
let indentRule = usesTabsForIndentation ? "\(indentSize) tabs" : "\(indentSize) spaces"
142-
if code.isEmpty {
143-
return """
144-
You are a senior programer in writing code in \(language.rawValue).
145-
146-
Please write a piece of code that meets my requirements. The indentation should be \(
147-
indentRule
148-
).
149-
150-
Please reply to me start with the code block, followed by a short description in 1-3 sentences about what you did \(
151-
textLanguage
152-
).
153-
"""
154-
} else {
155-
return """
156-
You are a senior programer in writing code in \(language.rawValue).
157-
158-
Please mutate the following code with my requirements. The indentation should be \(
159-
indentRule
160-
).
161-
162-
Please reply to me start with the code block followed by a short description about what you did in 1-3 sentences \(
163-
textLanguage
164-
).
165-
166-
```
167-
\(code)
168-
```
169-
"""
170-
}
171-
}()
172-
173-
let chatGPTService = ChatGPTService(systemPrompt: prompt)
174-
service = chatGPTService
175-
let stream = try await chatGPTService.send(content: requirement)
176-
return .init { continuation in
177-
Task {
178-
var content = ""
179-
do {
180-
for try await fragment in stream {
181-
content.append(fragment)
182-
continuation.yield(extractCodeAndDescription(from: content))
183-
}
184-
continuation.finish()
185-
} catch {
186-
continuation.finish(throwing: error)
187-
}
188-
}
189-
}
190-
}
191-
192-
func extractCodeAndDescription(from content: String) -> (code: String, description: String) {
193-
func extractCodeFromMarkdown(_ markdown: String) -> (code: String, endIndex: Int)? {
194-
let codeBlockRegex = try! NSRegularExpression(
195-
pattern: #"```(?:\w+)?[\n]([\s\S]+?)[\n]```"#,
196-
options: .dotMatchesLineSeparators
197-
)
198-
let range = NSRange(markdown.startIndex..<markdown.endIndex, in: markdown)
199-
if let match = codeBlockRegex.firstMatch(in: markdown, options: [], range: range) {
200-
let codeBlockRange = Range(match.range(at: 1), in: markdown)!
201-
return (String(markdown[codeBlockRange]), match.range(at: 0).upperBound)
202-
}
203-
204-
let incompleteCodeBlockRegex = try! NSRegularExpression(
205-
pattern: #"```(?:\w+)?[\n]([\s\S]+?)$"#,
206-
options: .dotMatchesLineSeparators
207-
)
208-
let range2 = NSRange(markdown.startIndex..<markdown.endIndex, in: markdown)
209-
if let match = incompleteCodeBlockRegex.firstMatch(
210-
in: markdown,
211-
options: [],
212-
range: range2
213-
) {
214-
let codeBlockRange = Range(match.range(at: 1), in: markdown)!
215-
return (String(markdown[codeBlockRange]), match.range(at: 0).upperBound)
216-
}
217-
return nil
218-
}
219-
220-
guard let (code, endIndex) = extractCodeFromMarkdown(content) else {
221-
return ("", "")
222-
}
223-
224-
func extractDescriptionFromMarkdown(_ markdown: String, startIndex: Int) -> String {
225-
let startIndex = markdown.index(markdown.startIndex, offsetBy: startIndex)
226-
guard startIndex < markdown.endIndex else { return "" }
227-
let range = startIndex..<markdown.endIndex
228-
let description = String(markdown[range])
229-
.trimmingCharacters(in: .whitespacesAndNewlines)
230-
return description
231-
}
232-
233-
let description = extractDescriptionFromMarkdown(content, startIndex: endIndex)
234-
235-
return (code, description)
236-
}
237-
}
238-
239-
final class CopilotPromptToCodeAPI: PromptToCodeAPI {
240-
func stopResponding() {
241-
fatalError()
242-
}
243-
244-
func modifyCode(
245-
code: String,
246-
language: CopilotLanguage,
247-
indentSize: Int,
248-
usesTabsForIndentation: Bool,
249-
requirement: String
250-
) async throws -> AsyncThrowingStream<(code: String, description: String), Error> {
251-
fatalError()
252-
}
253-
}

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,24 @@ final class WidgetDataSource {
8585
@discardableResult
8686
func createPromptToCode(
8787
for url: URL,
88-
code: String,
88+
projectURL: URL,
89+
selectedCode: String,
90+
allCode: String,
8991
selectionRange: CursorRange,
9092
language: CopilotLanguage,
9193
identSize: Int = 4,
9294
usesTabsForIndentation: Bool = false
9395
) async -> PromptToCodeService {
9496
let build = {
9597
let service = PromptToCodeService(
96-
code: code,
98+
code: selectedCode,
9799
selectionRange: selectionRange,
98100
language: language,
99101
identSize: identSize,
100-
usesTabsForIndentation: usesTabsForIndentation
102+
usesTabsForIndentation: usesTabsForIndentation,
103+
projectRootURL: projectURL,
104+
fileURL: url,
105+
allCode: allCode
101106
)
102107
let provider = PromptToCodeProvider(
103108
service: service,

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
341341
presenter.markAsProcessing(true)
342342
defer { presenter.markAsProcessing(false) }
343343
let fileURL = try await Environment.fetchCurrentFileURL()
344+
let projectURL = try await Environment.fetchCurrentProjectRootURL(fileURL)
344345
let codeLanguage = languageIdentifierFromFileURL(fileURL)
345346

346347
let (code, selection) = {
@@ -364,6 +365,9 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
364365
_ = await WidgetDataSource.shared.createPromptToCode(
365366
for: fileURL,
366367
code: code,
368+
projectURL: projectURL ?? fileURL,
369+
selectedCode: code,
370+
allCode: editor.content,
367371
selectionRange: selection,
368372
language: codeLanguage
369373
)

0 commit comments

Comments
 (0)