|
| 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 | +} |
0 commit comments