From c54db6d8ed34f471f78a57efaa63d262a5fadc74 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Thu, 29 Feb 2024 22:46:44 -0500 Subject: [PATCH] 1.2.28 (#133) --- build.gradle.kts | 2 +- gradle.properties | 2 +- .../aicoder/actions/generic/DiffChatAction.kt | 32 +- .../actions/generic/LineFilterChatAction.kt | 121 +++++ .../aicoder/actions/generic/WebDevAction.kt | 508 ++++++++++++++++++ .../aicoder/config/ActionSettingsRegistry.kt | 2 +- .../aicoder/util/SimpleDiffUtil.kt | 130 ----- src/main/resources/META-INF/plugin.xml | 19 + 8 files changed, 657 insertions(+), 159 deletions(-) create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt delete mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3ec16c6f..f5abc093 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,7 +26,7 @@ repositories { val kotlin_version = "1.9.21" val jetty_version = "11.0.18" val slf4j_version = "2.0.9" -val skyenet_version = "1.0.47" +val skyenet_version = "1.0.48" val remoterobot_version = "0.11.21" dependencies { diff --git a/gradle.properties b/gradle.properties index cb291b81..61ff304b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.2.27 +pluginVersion=1.2.28 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt index 79ba581a..a7eea799 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DiffChatAction.kt @@ -6,7 +6,7 @@ import com.github.simiacryptus.aicoder.actions.dev.AppServer import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.CodeChatSocketManager import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.SimpleDiffUtil +import com.github.simiacryptus.aicoder.util.addApplyDiffLinks import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.command.WriteCommandAction @@ -29,7 +29,6 @@ class DiffChatAction : BaseAction() { override fun handle(e: AnActionEvent) { val editor = e.getData(CommonDataKeys.EDITOR) ?: return - val session = StorageInterface.newGlobalID() val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return val document = editor.document @@ -66,32 +65,13 @@ class DiffChatAction : BaseAction() { get() = super.systemPrompt + """ Provide code patches in diff format within ```diff code blocks. The diff format should use + for line additions, - for line deletions. - The diff should include 2 lines of context before and after every change. + The diff should include sufficient context before every change to identify the location. """.trimIndent() - - val diffPattern = """(?s)(?() override fun renderResponse(response: String): String { - val matches = diffPattern.findAll(response).distinct() - val withLinks = matches.fold(response) { markdown, diffBlock -> - val diffVal = diffBlock.value - val hrefLink = hrefLink("Apply Diff") { - try { - if(fullPatch.contains(diffVal)) return@hrefLink - fullPatch.add(diffVal) - val newCode = fullPatch.fold(rawText) { - lines, patch -> SimpleDiffUtil.patch(lines, patch) - } - WriteCommandAction.runWriteCommandAction(e.project) { - document.replaceString(selectionStart, selectionEnd, newCode) - } - send(divInitializer(cancelable = false) + """
Diff Applied
""") - } catch (e: Throwable) { - log.warn("Error applying diff", e) - send(divInitializer(cancelable = false) + """
${e.message}
""") - } + val withLinks = addApplyDiffLinks(rawText, response) { newCode -> + WriteCommandAction.runWriteCommandAction(e.project) { + document.replaceString(selectionStart, selectionEnd, newCode) } - markdown.replace(diffVal, diffVal + "\n" + hrefLink) } val html = renderMarkdown(withLinks) return """
$html
""" @@ -130,4 +110,4 @@ class DiffChatAction : BaseAction() { } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt new file mode 100644 index 00000000..170a5e16 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/LineFilterChatAction.kt @@ -0,0 +1,121 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.ApplicationEvents +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.dev.AppServer +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.simiacryptus.skyenet.core.platform.ApplicationServices +import com.simiacryptus.skyenet.core.platform.Session +import com.simiacryptus.skyenet.core.platform.StorageInterface +import com.simiacryptus.skyenet.core.platform.User +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import com.simiacryptus.skyenet.webui.chat.ChatServer +import com.simiacryptus.skyenet.webui.chat.ChatSocketManager +import com.simiacryptus.skyenet.webui.session.SocketManager +import com.simiacryptus.skyenet.webui.util.MarkdownUtil.renderMarkdown +import org.slf4j.LoggerFactory +import java.awt.Desktop +import java.io.File + +class LineFilterChatAction : BaseAction() { + + val path = "/codeChat" + + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val session = StorageInterface.newGlobalID() + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val filename = FileDocumentManager.getInstance().getFile(editor.document)?.name ?: return + val code = editor.caretModel.primaryCaret.selectedText ?: editor.document.text + val lines = code.split("\n").toTypedArray() + val codelines = lines.withIndex().joinToString("\n") { (i, line) -> + "${i.toString().padStart(3, '0')} $line" + } + agents[session] = object : ChatSocketManager( + session = session, + model = AppSettingsState.instance.defaultChatModel(), + userInterfacePrompt = """ + |# `$filename` + | + |```$language + |$code + |``` + """.trimMargin().trim(), + systemPrompt = """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following code located in `$filename`: + | + |```$language + |$codelines + |``` + | + |Responses may use markdown formatting. Lines from the prompt can be included by using the line number in a response line (e.g. `\nLINE\n`). + | + |For example: + | + |```text + |001 + |## Injected subtitle + | + |025 + |026 + | + |013 + |014 + |``` + """.trimMargin(), + api = api, + applicationClass = ApplicationServer::class.java, + storage = ApplicationServices.dataStorageFactory(root), + ) { + override fun canWrite(user: User?): Boolean = true + override fun renderResponse(response: String): String { + return renderMarkdown(response.split("\n").joinToString("\n") { + when { + // Is numeric, use line if in range + it.toIntOrNull()?.let { i -> lines.indices.contains(i) } == true -> lines[it.toInt()] + // Otherwise, use response + else -> it + } + } +) } + } + + val server = AppServer.getServer(e.project) + val app = initApp(server, path) + app.sessions[session] = app.newSession(null, session) + + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + override fun isEnabled(event: AnActionEvent) = true + + companion object { + private val log = LoggerFactory.getLogger(LineFilterChatAction::class.java) + private val agents = mutableMapOf() + val root: File get() = File(ApplicationEvents.pluginHome, "code_chat") + private fun initApp(server: AppServer, path: String): ChatServer { + server.appRegistry[path]?.let { return it } + val socketServer = object : ApplicationServer(applicationName = "Code Chat", path = path) { + override val singleInput = false + override val stickyInput = true + override fun newSession(user: User?, session: Session) = agents[session]!! + } + server.addApp(path, socketServer) + return socketServer + } + + } +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt new file mode 100644 index 00000000..5b323d95 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/WebDevAction.kt @@ -0,0 +1,508 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.ApplicationEvents +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.dev.AppServer +import com.github.simiacryptus.aicoder.util.UITools +import com.github.simiacryptus.aicoder.util.addApplyDiffLinks +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.vfs.VirtualFile +import com.simiacryptus.jopenai.API +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.describe.Description +import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.proxy.ValidatedObject +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import com.simiacryptus.jopenai.util.JsonUtil +import com.simiacryptus.skyenet.AgentPatterns +import com.simiacryptus.skyenet.core.actors.ActorSystem +import com.simiacryptus.skyenet.core.actors.BaseActor +import com.simiacryptus.skyenet.core.actors.ParsedActor +import com.simiacryptus.skyenet.core.actors.SimpleActor +import com.simiacryptus.skyenet.core.platform.* +import com.simiacryptus.skyenet.core.platform.file.DataStorage +import com.simiacryptus.skyenet.webui.application.ApplicationInterface +import com.simiacryptus.skyenet.webui.application.ApplicationServer +import com.simiacryptus.skyenet.webui.chat.ChatServer +import com.simiacryptus.skyenet.webui.servlet.ToolServlet +import com.simiacryptus.skyenet.webui.session.SessionTask +import com.simiacryptus.skyenet.webui.util.MarkdownUtil +import org.slf4j.LoggerFactory +import java.awt.Desktop +import java.io.File +import java.util.concurrent.atomic.AtomicBoolean + +private val VirtualFile.toFile: File get() = File(this.path) + +class WebDevAction : BaseAction() { + + val path = "/webDev" + + override fun handle(e: AnActionEvent) { + val session = StorageInterface.newGlobalID() + val storage = ApplicationServices.dataStorageFactory(DiffChatAction.root) as DataStorage? + val selectedFile = UITools.getSelectedFolder(e) + if (null != storage && null != selectedFile) { + DataStorage.sessionPaths[session] = selectedFile.toFile + } + agents[session] = WebDevApp() + val server = AppServer.getServer(e.project) + val app = initApp(server, path) + app.sessions[session] = app.newSession(null, session) + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("$path/#$session")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + override fun isEnabled(event: AnActionEvent): Boolean { + if (UITools.getSelectedFile(event)?.isDirectory == false) return false + return super.isEnabled(event) + } + + open class WebDevApp( + applicationName: String = "Web Dev Assistant v1.1", + open val symbols: Map = mapOf(), + val temperature: Double = 0.1, + ) : ApplicationServer( + applicationName = applicationName, + path = "/webdev", + ) { + override fun userMessage( + session: Session, + user: User?, + userMessage: String, + ui: ApplicationInterface, + api: API + ) { + val settings = getSettings(session, user) ?: Settings() + if(api is ClientManager.MonitoredClient) api.budget = settings.budget ?: 2.00 + WebDevAgent( + api = api, + dataStorage = dataStorage, + session = session, + user = user, + ui = ui, + tools = settings.tools, + model = settings.model, + ).start( + userMessage = userMessage, + ) + } + + data class Settings( + val budget: Double? = 2.00, + val tools: List = emptyList(), + val model: ChatModels = ChatModels.GPT4Turbo, + ) + + override val settingsClass: Class<*> get() = Settings::class.java + + @Suppress("UNCHECKED_CAST") + override fun initSettings(session: Session): T? = Settings() as T + } + + class WebDevAgent( + val api: API, + dataStorage: StorageInterface, + session: Session, + user: User?, + val ui: ApplicationInterface, + val model: ChatModels = ChatModels.GPT35Turbo, + val tools: List = emptyList(), + val actorMap: Map> = mapOf( + ActorTypes.HtmlCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a skeleton HTML file for a rich javascript application. + The html file can reference needed CSS and JS files, which are will be located in the same directory as the html file. + Do not output the content of the resource files, only the html file. + """.trimIndent(), model = model + ), + ActorTypes.JavascriptCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a javascript file for use in a rich javascript application. + """.trimIndent(), model = model + ), + ActorTypes.CssCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a CSS file for use in a rich javascript application. + """.trimIndent(), model = model + ), + ActorTypes.ArchitectureDiscussionActor to ParsedActor( + parserClass = PageResourceListParser::class.java, + prompt = """ + Translate the user's idea into a detailed architecture for a simple web application. + Suggest specific frameworks/libraries to import and provide CDN links for them. + Specify user interactions and how the application will respond to them. + Identify key HTML classes and element IDs that will be used to bind the application to the HTML. + Identify coding styles and patterns to be used. + List all files to be created, and for each file, describe the public interface / purpose / content summary. + """.trimIndent(), + model = model + ), + ActorTypes.CodeReviewer to SimpleActor( + prompt = """ + Analyze the code summarized in the user's header-labeled code blocks. + Review, look for bugs, and provide fixes. + Provide implementations for missing functions. + + Response should use one or more code patches in diff format within ```diff code blocks. + Each diff should be preceded by a header that identifies the file being modified. + The diff format should use + for line additions, - for line deletions. + The diff should include 2 lines of context before and after every change. + + Example: + + Explanation text + + ### scripts/filename.js + ```diff + - const b = 2; + + const a = 1; + ``` + + Continued text + """.trimIndent(), + model = model, + ), + ActorTypes.EtcCodingActor to SimpleActor( + prompt = """ + You will translate the user request into a file for use in a web application. + """.trimIndent(), + model = model + ), + ), + ) : ActorSystem(actorMap, dataStorage, user, session) { + enum class ActorTypes { + HtmlCodingActor, + JavascriptCodingActor, + CssCodingActor, + ArchitectureDiscussionActor, + CodeReviewer, + EtcCodingActor, + } + + val architectureDiscussionActor by lazy { getActor(ActorTypes.ArchitectureDiscussionActor) as ParsedActor } + val htmlActor by lazy { getActor(ActorTypes.HtmlCodingActor) as SimpleActor } + val javascriptActor by lazy { getActor(ActorTypes.JavascriptCodingActor) as SimpleActor } + val cssActor by lazy { getActor(ActorTypes.CssCodingActor) as SimpleActor } + val codeReviewer by lazy { getActor(ActorTypes.CodeReviewer) as SimpleActor } + val codeFiles = mutableMapOf() + val etcActor by lazy { getActor(ActorTypes.EtcCodingActor) as SimpleActor } + + fun start( + userMessage: String, + ) { + val architectureResponse = AgentPatterns.iterate( + input = userMessage, + heading = userMessage, + actor = architectureDiscussionActor, + toInput = { listOf(it) }, + api = api, + ui = ui, + outputFn = { task, design -> + task.add(MarkdownUtil.renderMarkdown("${design.text}\n\n```json\n${JsonUtil.toJson(design.obj)}\n```")) + } + ) + + val task = ui.newTask() + try { + val toolSpecs = tools.map { ToolServlet.tools.find { t -> t.path == it } } + .joinToString("\n\n") { it?.let { JsonUtil.toJson(it.openApiDescription) } ?: "" } + var messageWithTools = userMessage + if (toolSpecs.isNotBlank()) messageWithTools += "\n\nThese services are available:\n$toolSpecs" + task.echo(MarkdownUtil.renderMarkdown("```json\n${JsonUtil.toJson(architectureResponse.obj)}\n```")) + architectureResponse.obj.resources.filter { + !it.path!!.startsWith("http") + }.forEach { (path, description) -> + val task = ui.newTask() + when (path!!.split(".").last().lowercase()) { + + "js" -> draftResourceCode( + task, + javascriptActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + javascriptActor, + path!!, "js", "javascript" + ) + + + "css" -> draftResourceCode( + task, + cssActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + cssActor, + path + ) + + "html" -> draftResourceCode( + task, + htmlActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + htmlActor, + path + ) + + else -> draftResourceCode( + task, + etcActor.chatMessages( + listOf( + messageWithTools, + architectureResponse.text, + "Render $path - $description" + ) + ), + etcActor, path) + + } + } + // Apply codeReviewer + fun codeSummary() = codeFiles.entries.joinToString("\n\n") { (path, code) -> + "# $path\n```${ + path.split('.').last() + }\n$code\n```" + } + + + fun outputFn(task: SessionTask, design: String): StringBuilder? { + //val task = ui.newTask() + return task.complete( + MarkdownUtil.renderMarkdown( + ui.socketManager.addApplyDiffLinks( + codeFiles, + design + ) { newCodeMap -> + newCodeMap.forEach { (path, newCode) -> + val prev = codeFiles[path] + if (prev != newCode) { + codeFiles[path] = newCode + task.complete( + "$path Updated" + ) + } + } + }) + ) + } + try { + var task = ui.newTask() + task.add(message = MarkdownUtil.renderMarkdown(codeSummary())) + var design = codeReviewer.answer(listOf(element = codeSummary()), api = api) + outputFn(task, design) + var textInputHandle: StringBuilder? = null + var textInput: String? = null + val feedbackGuard = AtomicBoolean(false) + textInput = ui.textInput { userResponse -> + if (feedbackGuard.getAndSet(true)) return@textInput + textInputHandle?.clear() + task.complete() + task = ui.newTask() + task.echo(MarkdownUtil.renderMarkdown(userResponse)) + val codeSummary = codeSummary() + task.add(MarkdownUtil.renderMarkdown(codeSummary)) + design = codeReviewer.respond( + messages = codeReviewer.chatMessages( + listOf( + codeSummary, + userResponse, + ) + ), + input = listOf(element = codeSummary), + api = api + ) + outputFn(task, design) + textInputHandle = task.complete(textInput!!) + feedbackGuard.set(false) + } + textInputHandle = task.complete(textInput) + } catch (e: Throwable) { + val task = ui.newTask() + task.error(ui = ui, e = e) + throw e + } + } catch (e: Throwable) { + log.warn("Error", e) + task.error(ui, e) + } + } + + private fun draftResourceCode( + task: SessionTask, + request: Array, + actor: SimpleActor, + path: String, + vararg languages: String = arrayOf(path.split(".").last().lowercase()), + ) { + try { + var code = actor.respond(emptyList(), api, *request) + languages.forEach { language -> + if (code.contains("```$language")) code = code.substringAfter("```$language").substringBefore("```") + } + try { + task.add(MarkdownUtil.renderMarkdown("```${languages.first()}\n$code\n```")) + task.add("$path Updated") + codeFiles[path] = code + val request1 = (request.toList() + + listOf( + ApiModel.ChatMessage(ApiModel.Role.assistant, code.toContentList()), + )).toTypedArray() + val formText = StringBuilder() + var formHandle: StringBuilder? = null + formHandle = task.add( + """ + |
+ |${ + ui.hrefLink("♻", "href-link regen-button") { + val task = ui.newTask() + responseAction(task, "Regenerating...", formHandle!!, formText) { + draftResourceCode( + task, + request1.dropLastWhile { it.role == ApiModel.Role.assistant }.toTypedArray(), + actor, path, *languages + ) + } + } + } + |
+ |${ + ui.textInput { feedback -> + responseAction(task, "Revising...", formHandle!!, formText) { + //val task = ui.newTask() + try { + task.echo(MarkdownUtil.renderMarkdown(feedback)) + draftResourceCode( + task, (request1.toList() + listOf( + code to ApiModel.Role.assistant, + feedback to ApiModel.Role.user, + ).filter { it.first.isNotBlank() } + .map { + ApiModel.ChatMessage( + it.second, + it.first.toContentList() + ) + }).toTypedArray(), actor, path, *languages + ) + } catch (e: Throwable) { + log.warn("Error", e) + task.error(ui, e) + } + } + } + } + """.trimMargin(), className = "reply-message" + ) + formText.append(formHandle.toString()) + formHandle.toString() + task.complete() + } catch (e: Throwable) { + task.error(ui, e) + log.warn("Error", e) + } + } catch (e: Throwable) { + log.warn("Error", e) + val error = task.error(ui, e) + var regenButton: StringBuilder? = null + regenButton = task.complete(ui.hrefLink("♻", "href-link regen-button") { + regenButton?.clear() + val header = task.header("Regenerating...") + draftResourceCode(task, request, actor, path, *languages) + header?.clear() + error?.clear() + task.complete() + }) + } + } + + private fun responseAction( + task: SessionTask, + message: String, + formHandle: StringBuilder?, + formText: StringBuilder, + fn: () -> Unit = {} + ) { + formHandle?.clear() + val header = task.header(message) + try { + fn() + } finally { + header?.clear() + var revertButton: StringBuilder? = null + revertButton = task.complete(ui.hrefLink("↩", "href-link regen-button") { + revertButton?.clear() + formHandle?.append(formText) + task.complete() + }) + } + } + } + + companion object { + private val log = LoggerFactory.getLogger(WebDevAction::class.java) + private val agents = mutableMapOf() + val root: File get() = File(ApplicationEvents.pluginHome, "code_chat") + private fun initApp(server: AppServer, path: String): ChatServer { + server.appRegistry[path]?.let { return it } + val socketServer = object : ApplicationServer(applicationName = "Code Chat", path = path) { + override val singleInput = true + override val stickyInput = false + override fun newSession(user: User?, session: Session) = agents[session]!!.newSession(user, session) + } + server.addApp(path, socketServer) + return socketServer + } + + interface PageResourceListParser : java.util.function.Function { + @Description("Parse out a list of files and descriptions in this project") + override fun apply(html: String): PageResourceList + } + + data class PageResourceList( + @Description("List of resources in this project; don't forget the index.html file!") + val resources: List = emptyList() + ) : ValidatedObject { + override fun validate(): String? = when { + resources.isEmpty() -> "Resources are required" + resources.any { it.validate() != null } -> "Invalid resource" + else -> null + } + } + + data class PageResource( + val path: String? = "", + val description: String? = "" + ) : ValidatedObject { + override fun validate(): String? = when { + path.isNullOrBlank() -> "Path is required" + path.contains(" ") -> "Path cannot contain spaces" + !path.contains(".") -> "Path must contain a file extension" + else -> null + } + } + + } + +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt index e7ae8ae7..a7d9eb42 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -11,7 +11,7 @@ import java.util.stream.Collectors class ActionSettingsRegistry { val actionSettings: MutableMap = HashMap() - private val version = 2.0042 + private val version = 2.0049 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt deleted file mode 100644 index 1da27a99..00000000 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/SimpleDiffUtil.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.github.simiacryptus.aicoder.util - -import org.apache.commons.text.similarity.LevenshteinDistance - -object SimpleDiffUtil { - - - fun patch(source: String, patch: String): String { - val sourceLines = source.lines() - val patchLines = patch.lines() - - // This will hold the final result - val result = mutableListOf() - - // This will keep track of the current line in the source file - var sourceIndex = 0 - - // Process each line in the patch - for (patchLine in patchLines.map { it.trim() }) { - when { - // If the line starts with "---" or "+++", it's a file indicator line, skip it - patchLine.startsWith("---") || patchLine.startsWith("+++") -> continue - - // If the line starts with "@@", it's a hunk header - patchLine.startsWith("@@") -> continue - - // If the line starts with "-", it's a deletion, skip the corresponding source line but otherwise treat it as a context line - patchLine.startsWith("-") -> { - sourceIndex = onDelete(patchLine, sourceIndex, sourceLines, result) - } - - // If the line starts with "+", it's an addition, add it to the result - patchLine.startsWith("+") -> { - result.add(patchLine.substring(1)) - } - - // \d+\: ___ is a line number, strip it - patchLine.matches(Regex("\\d+:.*")) -> { - sourceIndex = onContextLine(patchLine.substringAfter(":"), sourceIndex, sourceLines, result) - } - - // it's a context line, advance the source cursor - else -> { - sourceIndex = onContextLine(patchLine, sourceIndex, sourceLines, result) - } - } - } - - // Append any remaining lines from the source file - while (sourceIndex < sourceLines.size) { - result.add(sourceLines[sourceIndex]) - sourceIndex++ - } - - return result.joinToString("\n") - } - - private fun onDelete( - patchLine: String, - sourceIndex: Int, - sourceLines: List, - result: MutableList - ): Int { - var sourceIndex1 = sourceIndex - val delLine = patchLine.substring(1) - val sourceIndexSearch = lookAheadFor(sourceIndex1, sourceLines, delLine) - if (sourceIndexSearch > 0 && sourceIndexSearch + 1 < sourceLines.size) { - val contextChunk = sourceLines.subList(sourceIndex1, sourceIndexSearch) - result.addAll(contextChunk) - sourceIndex1 = sourceIndexSearch + 1 - } else { - println("Deletion line not found in source file: $delLine") - // Ignore - } - return sourceIndex1 - } - - private fun onContextLine( - patchLine: String, - sourceIndex: Int, - sourceLines: List, - result: MutableList - ): Int { - var sourceIndex1 = sourceIndex - val sourceIndexSearch = lookAheadFor(sourceIndex1, sourceLines, patchLine) - if (sourceIndexSearch > 0 && sourceIndexSearch + 1 < sourceLines.size) { - val contextChunk = sourceLines.subList(sourceIndex1, sourceIndexSearch + 1) - result.addAll(contextChunk) - sourceIndex1 = sourceIndexSearch + 1 - } else { - println("Context line not found in source file: $patchLine") - // Ignore - } - return sourceIndex1 - } - - private fun lookAheadFor( - sourceIndex: Int, - sourceLines: List, - patchLine: String - ): Int { - var sourceIndexSearch = sourceIndex - while (sourceIndexSearch < sourceLines.size) { - if (lineMatches(patchLine, sourceLines[sourceIndexSearch++])) return sourceIndexSearch - 1 - } - return - 1 - } - - private fun lineMatches( - a: String, - b: String, - factor: Double = 0.3 - ): Boolean { - val threshold = (Math.max(a.trim().length, b.trim().length) * factor).toInt() - val levenshteinDistance = LevenshteinDistance(5) - val dist = levenshteinDistance.apply(a.trim(), b.trim()) - if (dist >= 0) { - if (dist <= threshold) { - return true - } else { - return false - } - } else { - return false - } - } -} - - - diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d215424d..d20708e8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -110,6 +110,25 @@ --> + + + + + + + +