diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 033c076..247ccb3 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -4,6 +4,7 @@ <mapping directory="" vcs="Git" /> <mapping directory="$PROJECT_DIR$/gui" vcs="Git" /> <mapping directory="$PROJECT_DIR$/tools" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/tools/sonar-rspec" vcs="Git" /> <mapping directory="$PROJECT_DIR$/workflows" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f6a50f2..890b7e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ tasks.register<Copy>("copyTools") { "python-3.11.6-embed-amd64/**", "site-packages/**", "sonar-rspec/**", + "code-editor/**", "replace.sh" ) } diff --git a/gui b/gui index 03089d0..d97b8a5 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit 03089d049a326b32fb5d04c140c1e04e28c7a1eb +Subproject commit d97b8a5bad9f7844f2fc2d5c939bc95a44fb98ee diff --git a/src/main/kotlin/ai/devchat/common/CommandLine.kt b/src/main/kotlin/ai/devchat/common/CommandLine.kt new file mode 100644 index 0000000..540a134 --- /dev/null +++ b/src/main/kotlin/ai/devchat/common/CommandLine.kt @@ -0,0 +1,16 @@ +package ai.devchat.common + +import java.io.BufferedReader + + +data class CommandResult(val output: String, val errors: String, val exitCode: Int) + +object CommandLine { + fun exec(vararg command: String): CommandResult { + val process = ProcessBuilder(*command).start() + val output = process.inputStream.bufferedReader(charset=Charsets.UTF_8).use(BufferedReader::readText) + val errors = process.errorStream.bufferedReader(charset=Charsets.UTF_8).use(BufferedReader::readText) + val exitCode = process.waitFor() + return CommandResult(output, errors, exitCode) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ai/devchat/common/OSInfo.kt b/src/main/kotlin/ai/devchat/common/OSInfo.kt index f571767..1860c94 100644 --- a/src/main/kotlin/ai/devchat/common/OSInfo.kt +++ b/src/main/kotlin/ai/devchat/common/OSInfo.kt @@ -4,7 +4,7 @@ import java.util.* object OSInfo { val OS_NAME: String = System.getProperty("os.name").lowercase(Locale.getDefault()) - val OS_ARCH: String = System.getProperty("os.arch") + val OS_ARCH: String = System.getProperty("os.arch").lowercase(Locale.getDefault()) val isWindows: Boolean = OS_NAME.contains("win") val platform: String = when { diff --git a/src/main/kotlin/ai/devchat/common/PathUtils.kt b/src/main/kotlin/ai/devchat/common/PathUtils.kt index dec5664..f5df365 100644 --- a/src/main/kotlin/ai/devchat/common/PathUtils.kt +++ b/src/main/kotlin/ai/devchat/common/PathUtils.kt @@ -1,5 +1,6 @@ package ai.devchat.common +import java.io.File import java.io.IOException import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes @@ -13,22 +14,41 @@ object PathUtils { val mambaWorkPath = Paths.get(workPath, "mamba").toString() val mambaBinPath = Paths.get(mambaWorkPath, "micromamba").toString() val toolsPath: String = Paths.get(workPath, "tools").toString() + val codeEditorBinary: String = "${when { + OSInfo.OS_ARCH.contains("aarch") || OSInfo.OS_ARCH.contains("arm") -> "aarch64" + else -> "x86_64" + }}-${when { + OSInfo.OS_NAME.contains("win") -> "pc-windows-msvc" + OSInfo.OS_NAME.contains("darwin") || OSInfo.OS_NAME.contains("mac") -> "apple-darwin" + OSInfo.OS_NAME.contains("linux") -> "unknown-linux-musl" + else -> throw RuntimeException("Unsupported OS: ${OSInfo.OS_NAME}") + }}-code_editor" + if (OSInfo.isWindows) ".exe" else "" - fun copyResourceDirToPath(resourceDir: String, outputDir: String, overwrite: Boolean = false): String { - val uri = javaClass.getResource(resourceDir)!!.toURI() + fun copyResourceDirToPath(resourcePath: String, outputPath: String, overwrite: Boolean = false): String { + val uri = javaClass.getResource(resourcePath)?.toURI() ?: throw IllegalArgumentException( + "Resource not found: $resourcePath" + ) val sourcePath = if (uri.scheme == "jar") { val fileSystem = try { FileSystems.newFileSystem(uri, emptyMap<String, Any>()) } catch (e: FileSystemAlreadyExistsException) { FileSystems.getFileSystem(uri) } - fileSystem.getPath("/$resourceDir") + fileSystem.getPath("/$resourcePath") } else { Paths.get(uri) } - val targetPath = Paths.get(outputDir) - if (!Files.exists(targetPath)) Files.createDirectories(targetPath) - if (overwrite) targetPath.toFile().deleteRecursively() + + val targetPath = Paths.get(outputPath) + if (!Files.exists(targetPath.parent)) Files.createDirectories(targetPath.parent) + if (overwrite && Files.exists(targetPath)) targetPath.toFile().deleteRecursively() + + // Handle single file copying + if (Files.isRegularFile(sourcePath)) { + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING) + targetPath.toFile().setExecutable(true) + return targetPath.toString() + } Files.walkFileTree(sourcePath, object : SimpleFileVisitor<Path>() { @Throws(IOException::class) @@ -52,4 +72,10 @@ object PathUtils { return targetPath.toString() } + + fun createTempFile(prefix: String, content: String): String { + val tempFile = File.createTempFile(prefix, "") + tempFile.writeText(content) + return tempFile.absolutePath + } } diff --git a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt b/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt index 0d783c4..34de51c 100644 --- a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt +++ b/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt @@ -39,6 +39,11 @@ class DevChatSetupThread : Thread() { private fun setup(envManager: PythonEnvManager) { val overwrite = devChatVersion != DevChatState.instance.lastVersion PathUtils.copyResourceDirToPath("/tools/site-packages", PathUtils.sitePackagePath, overwrite) + PathUtils.copyResourceDirToPath( + "/tools/code-editor/${PathUtils.codeEditorBinary}", + Paths.get(PathUtils.toolsPath, PathUtils.codeEditorBinary).toString(), + overwrite + ) PathUtils.copyResourceDirToPath( "/tools/sonar-rspec", Paths.get(PathUtils.toolsPath, "sonar-rspec").toString(), diff --git a/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt b/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt index 384010c..f5e5e7d 100644 --- a/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt +++ b/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt @@ -1,5 +1,8 @@ package ai.devchat.plugin +import ai.devchat.common.CommandLine +import ai.devchat.common.Log +import ai.devchat.common.PathUtils import ai.devchat.core.DevChatActions import ai.devchat.core.handlers.CodeDiffApplyHandler import com.intellij.diff.DiffContentFactory @@ -10,28 +13,45 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.ui.DialogWrapper import java.awt.event.ActionEvent +import java.io.File +import java.nio.file.Paths import javax.swing.Action import javax.swing.JComponent class DiffViewerDialog( val editor: Editor, - private val newText: String + private var newText: String, + autoEdit: Boolean = false ) : DialogWrapper(editor.project) { + private var startOffset: Int = 0 + private var endOffset: Int = editor.document.textLength + private var localContent: String = editor.document.text + init { - super.init() title = "Confirm Changes" + val selectionModel = editor.selectionModel + val maxIdx = editor.document.textLength + if (!autoEdit && selectionModel.hasSelection()) { + startOffset = selectionModel.selectionStart.coerceIn(0, maxIdx) + endOffset = selectionModel.selectionEnd.coerceIn(0, maxIdx).coerceAtLeast(startOffset) + localContent = editor.selectionModel.selectedText ?: "" + } + if (autoEdit) { + try { + newText = editText() + } catch(e: Exception) { + Log.warn("Failed to edit code: $e") + } + } + super.init() } override fun createCenterPanel(): JComponent { - val virtualFile = FileDocumentManager.getInstance().getFile(editor.document) - val fileType = virtualFile!!.fileType - val localContent = if (editor.selectionModel.hasSelection()) { - editor.selectionModel.selectedText - } else editor.document.text + val fileType = FileDocumentManager.getInstance().getFile(editor.document)!!.fileType val contentFactory = DiffContentFactory.getInstance() val diffRequest = SimpleDiffRequest( "Code Diff", - contentFactory.create(localContent!!, fileType), + contentFactory.create(localContent, fileType), contentFactory.create(newText, fileType), "Old code", "New code" @@ -45,27 +65,24 @@ class DiffViewerDialog( return arrayOf(cancelAction, object: DialogWrapperAction("Apply") { override fun doAction(e: ActionEvent?) { - val selectionModel = editor.selectionModel - val document = editor.document - val startOffset: Int? - val endOffset: Int? - if (selectionModel.hasSelection()) { - startOffset = selectionModel.selectionStart - endOffset = selectionModel.selectionEnd - } else { - startOffset = 0 - endOffset = document.textLength - 1 - } WriteCommandAction.runWriteCommandAction(editor.project) { - // Ensure offsets are valid - val safeStartOffset = startOffset.coerceIn(0, document.textLength) - val safeEndOffset = endOffset.coerceIn(0, document.textLength).coerceAtLeast(safeStartOffset) - // Replace the selected range with new text - document.replaceString(safeStartOffset, safeEndOffset, newText) + editor.document.replaceString(startOffset, endOffset, newText) } CodeDiffApplyHandler(DevChatActions.CODE_DIFF_APPLY_REQUEST,null, null).executeAction() close(OK_EXIT_CODE) } }) } + + private fun editText(): String { + val srcTempFile = PathUtils.createTempFile("code_editor_src_", editor.document.text) + val newTempFile = PathUtils.createTempFile("code_editor_new_", newText) + val resultTempFile = PathUtils.createTempFile("code_editor_res_", "") + val codeEditorPath = Paths.get(PathUtils.toolsPath, PathUtils.codeEditorBinary).toString() + val result = CommandLine.exec(codeEditorPath, srcTempFile, newTempFile, resultTempFile) + require(result.exitCode == 0) { + throw Exception("Code editor failed with exit code ${result.exitCode}") + } + return File(resultTempFile).readText() + } } \ No newline at end of file diff --git a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt index b419179..42e9420 100644 --- a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt +++ b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt @@ -58,6 +58,8 @@ const val START_PORT: Int = 31800 @Serializable data class ReqLocation(val abspath: String, val line: Int, val character: Int) @Serializable +data class DiffApplyRequest(val filepath: String?, val content: String?, val autoedit: Boolean?) +@Serializable data class Position(val line: Int, val character: Int) @Serializable data class Range(val start: Position, val end: Position) @@ -100,7 +102,6 @@ class IDEServer(private var project: Project) { call.respond(Result(definitions)) } - post("/references") { val body: ReqLocation = call.receive() val references = withContext(Dispatchers.IO) { @@ -242,20 +243,17 @@ class IDEServer(private var project: Project) { } ?: call.respond(HttpStatusCode.NoContent) } post("/diff_apply") { - val body = call.receive<Map<String, String>>() - val filePath: String? = body["filepath"] - var content: String? = body["content"] - if (content.isNullOrEmpty() && !filePath.isNullOrEmpty()) { - content = File(filePath).readText() - } - if (content.isNullOrEmpty()) { - content = "" - } + val body: DiffApplyRequest = call.receive() + val filePath: String? = body.filepath + val content = body.content.takeUnless { it.isNullOrEmpty() } + ?: filePath?.takeUnless { it.isEmpty() }?.let { File(it).readText() } + ?: "" + val autoEdit: Boolean = body.autoedit ?: false var editor: Editor? = null ApplicationManager.getApplication().invokeAndWait { editor = FileEditorManager.getInstance(project).selectedTextEditor } - editor?.diffWith(content) + editor?.diffWith(content, autoEdit) call.respond(Result(true)) } post("/ide_logging") { @@ -348,9 +346,9 @@ fun Editor.visibleRange(): LocationWithText { ) } -fun Editor.diffWith(newText: String) { +fun Editor.diffWith(newText: String, autoEdit: Boolean) { ApplicationManager.getApplication().invokeLater { - val dialog = DiffViewerDialog(this, newText) + val dialog = DiffViewerDialog(this, newText, autoEdit) dialog.show() } } diff --git a/tools b/tools index c4f335e..0557f55 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit c4f335e486a33f8161d4866b1d0e5600391a0434 +Subproject commit 0557f55a8d5dadabcf225a2b7a24db9db120f895