Skip to content

Commit

Permalink
Merge pull request #189 from devchat-ai/code-auto-edit
Browse files Browse the repository at this point in the history
Code auto edit
  • Loading branch information
pplam authored Jul 9, 2024
2 parents dfa945d + 63ead41 commit 7244927
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 46 deletions.
1 change: 1 addition & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ tasks.register<Copy>("copyTools") {
"python-3.11.6-embed-amd64/**",
"site-packages/**",
"sonar-rspec/**",
"code-editor/**",
"replace.sh"
)
}
Expand Down
2 changes: 1 addition & 1 deletion gui
16 changes: 16 additions & 0 deletions src/main/kotlin/ai/devchat/common/CommandLine.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/ai/devchat/common/OSInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 32 additions & 6 deletions src/main/kotlin/ai/devchat/common/PathUtils.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
65 changes: 41 additions & 24 deletions src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand All @@ -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()
}
}
24 changes: 11 additions & 13 deletions src/main/kotlin/ai/devchat/plugin/IDEServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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()
}
}
Expand Down

0 comments on commit 7244927

Please sign in to comment.