Skip to content

Commit 937cd23

Browse files
committed
feat(commands): replace PATCH with EDIT_FILE command #408
Replace the PATCH command with a new EDIT_FILE command that provides structured file editing capabilities. The new command uses target_file, instructions, and code_edit parameters with clear context markers (// ... existing code ...) for precise modifications. Updates all related processors, factories, templates, and documentation to use the new edit_file syntax.
1 parent efdbe21 commit 937cd23

File tree

12 files changed

+284
-44
lines changed

12 files changed

+284
-44
lines changed

core/src/main/kotlin/cc/unitmesh/devti/bridge/BridgeToolProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import cc.unitmesh.devti.provider.toolchain.ToolchainFunctionProvider
77
import com.intellij.openapi.project.Project
88

99
object BridgeToolProvider {
10-
val Tools = setOf(STRUCTURE, RIPGREP_SEARCH, DATABASE, DIR, WRITE, PATCH, FILE)
10+
val Tools = setOf(STRUCTURE, RIPGREP_SEARCH, DATABASE, DIR, WRITE, FILE, EDIT_FILE)
1111

1212
suspend fun collect(project: Project): List<AgentTool> {
1313
val commonTools = Tools

core/src/main/kotlin/cc/unitmesh/devti/command/dataprovider/BuiltinCommand.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ enum class BuiltinCommand(
5656
AllIcons.Vcs.Patch_file,
5757
false
5858
),
59+
EDIT_FILE(
60+
"edit_file",
61+
"Apply structured file edits using target_file, instructions, and code_edit parameters. Designed for precise code modifications with clear context markers. Use // ... existing code ... to represent unchanged sections. Ideal for targeted edits with explicit instructions.",
62+
AllIcons.Actions.Edit,
63+
false
64+
),
5965
RUN(
6066
"run",
6167
"Execute IntelliJ IDEA's built-in build and test commands. Use for running Gradle tasks, Maven goals, npm scripts, or test suites. Essential for validating code changes and ensuring project builds correctly. Returns execution output, exit codes, and error information.",

core/src/main/kotlin/cc/unitmesh/devti/sketch/AutoSketchMode.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class AutoSketchMode(val project: Project) {
8989
)
9090

9191
of += setOf(
92-
PATCH, DATABASE, WRITE
92+
EDIT_FILE, DATABASE, WRITE
9393
)
9494

9595
return of

core/src/main/resources/genius/en/code/bridge.vm

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Here is an example output to the USER:
127127
根据您的项目结构,我建议您先使用 gogocode 迁移工具进行迁移,这是一个非常好的选择。先提供 gogocode 迁移方案,然后等待用户的回复。与此同时,
128128
我将更新项目的依赖信息:
129129
<devin>
130-
./gradlew :build --refresh-dependencies [注释:需要,用户先打 patch,再刷新项目依赖]
130+
./gradlew :build --refresh-dependencies [注释:需要,用户先编辑文件,再刷新项目依赖]
131131
</devin>
132132

133133
</you.answer4>
@@ -138,8 +138,11 @@ Here is an example output to the USER:
138138
// ... 优先使用 DevIns 指令来修改代码和提供范例
139139
现在,我将读取 xx 文件,然后为您提供迁移示例:
140140
<devin>
141-
```patch
142-
//
141+
/edit_file:xx_file.java
142+
```
143+
target_file: "xx_file.java"
144+
instructions: "Migrate code structure"
145+
code_edit: "// migration code"
143146
```
144147
</devin>
145148
</your.answer5>

core/src/main/resources/genius/en/code/sketch.vm

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,28 +132,34 @@ have created a routes.py and main.js file, and updated the main.html file.
132132

133133
根据我先前的计划,还有一些步骤需要完成。如果您遇到任何错误或想添加新功能,请告诉我!
134134

135-
// then you can start coding with DevIn language. When you need to or patch, write execute the code, you should use the DevIn language
135+
// then you can start coding with DevIn language. When you need to edit files, you should use the DevIn language
136136
// If you need to create a new file, you should use `/write` command, then write the code in the code block
137137
<devin>
138-
/patch:src/main/route.py [注释:当确定文件不存在时,才能创建文件]
139-
```patch
140-
Index: src/main/route.py
141-
// the rest code
138+
/edit_file:src/main/route.py [注释:当确定文件不存在时,才能创建文件]
139+
```
140+
target_file: "src/main/route.py"
141+
instructions: "Create route.py file with upload and query endpoints"
142+
code_edit: "// the rest code"
142143
```
143144
</devin>
144-
// patch to call tools for step 3 with DevIn language, should use `<devin />` tag with DevIn language
145-
// 如果要应用补丁,请使用 `/patch` 命令,然后在代码块中编写补丁,每个 patch 只能修改一个文件,并且使用独立的 `<devin />` 标签
145+
// edit_file to call tools for step 3 with DevIn language, should use `<devin />` tag with DevIn language
146+
// 如果要编辑文件,请使用 `/edit_file` 命令,然后在代码块中编写编辑内容,每个 edit_file 只能修改一个文件,并且使用独立的 `<devin />` 标签
146147
<devin>
147-
/patch:src/main/index.html
148-
```patch
149-
Index: src/main/index.html
150-
...
148+
/edit_file:src/main/index.html
149+
```
150+
target_file: "src/main/index.html"
151+
instructions: "Update index.html file"
152+
code_edit: "..."
151153
```
152154
</devin>
153155
// step 4.1, call tools to create test code and run test code
154156
<devin>
155-
/patch:src/test/test_routes.py
156-
```patch
157+
/edit_file:src/test/test_routes.py
158+
```
159+
target_file: "src/test/test_routes.py"
160+
instructions: "Create test file for routes"
161+
code_edit: "// the rest code"
162+
```
157163
Index: src/test/test_routes.py
158164
// the rest code
159165
```
@@ -219,10 +225,11 @@ feat: add delete blog functionality
219225
</devin>
220226
// 其它代码修改
221227
<devin>
222-
/patch:SketchRunContext.java
223-
```patch
224-
Index: SketchRunContext.java
225-
...
228+
/edit_file:SketchRunContext.java
229+
```
230+
target_file: "SketchRunContext.java"
231+
instructions: "Optimize SketchRunContext code structure"
232+
code_edit: "..."
226233
```
227234
</devin>
228235
// 你需要根据上下文来生成启动命令,可以尽可能使用 bash 命令来启动应用程序

core/src/main/resources/genius/zh/code/bridge.vm

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Here is an example output to the USER:
127127
根据您的项目结构,我建议您先使用 gogocode 迁移工具进行迁移,这是一个非常好的选择。先提供 gogocode 迁移方案,然后等待用户的回复。与此同时,
128128
我将更新项目的依赖信息:
129129
```bash
130-
./gradlew :build --refresh-dependencies [注释:需要,用户先打 patch,再刷新项目依赖]
130+
./gradlew :build --refresh-dependencies [注释:需要,用户先编辑文件,再刷新项目依赖]
131131
```
132132

133133
</you.answer4>
@@ -138,8 +138,11 @@ Here is an example output to the USER:
138138
// ... 优先使用 DevIns 指令来修改代码和提供范例
139139
现在,我将读取 xx 文件,然后为您提供迁移示例:
140140
<devin>
141-
```patch
142-
//
141+
/edit_file:xx_file.java
142+
```
143+
target_file: "xx_file.java"
144+
instructions: "迁移代码结构"
145+
code_edit: "// 迁移代码"
143146
```
144147
</devin>
145148
</your.answer5>

core/src/main/resources/genius/zh/code/sketch.vm

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,23 @@ say 'I will edit your file'.
111111
# 第一步. 创建 routes.py
112112
我将创建了 routes.py 来定义 "/upload" 和 "/query" 端点。此外,我还添加了 "/" 作为 main.html 的端点。
113113
<devin>
114-
/patch:src/main/route.py [注释:当确定文件不存在时,才能创建文件]
115-
```patch
116-
Index: src/main/route.py
117-
// the rest code
114+
/edit_file:src/main/route.py [注释:当确定文件不存在时,才能创建文件]
115+
```
116+
target_file: "src/main/route.py"
117+
instructions: "Create route.py file with upload and query endpoints"
118+
code_edit: "// the rest code"
118119
```
119120
</devin>
120121
# 第二步. 创建 main.js
121122
我将创建一个专用的 main.js 文件来存储所有的交互式前端代码。它定义了显示窗口和按钮的 UI 元素,并为这些按钮创建了事件监听器。
122-
// patch to call tools for step 3 with DevIn language, should use `<devin />` tag with DevIn language
123-
// 如果要应用补丁,请使用 `/patch` 命令,然后在代码块中编写补丁,每个 patch 只能修改一个文件,并且使用独立的 `<devin />` 标签
123+
// edit_file to call tools for step 3 with DevIn language, should use `<devin />` tag with DevIn language
124+
// 如果要编辑文件,请使用 `/edit_file` 命令,然后在代码块中编写编辑内容,每个 edit_file 只能修改一个文件,并且使用独立的 `<devin />` 标签
124125
<devin>
125-
/patch:src/main/index.html
126-
```patch
127-
Index: src/main/index.html
128-
...
126+
/edit_file:src/main/index.html
127+
```
128+
target_file: "src/main/index.html"
129+
instructions: "Update index.html file"
130+
code_edit: "..."
129131
```
130132
</devin>
131133

@@ -136,10 +138,11 @@ Index: src/main/index.html
136138
我将使用 Flask 的测试框架编写自动化测试用例,以确保应用程序的功能正常。
137139
// step 4.1, call tools to create test code and run test code
138140
<devin>
139-
/patch:src/test/test_routes.py
140-
```patch
141-
Index: src/test/test_routes.py
142-
// the rest code
141+
/edit_file:src/test/test_routes.py
142+
```
143+
target_file: "src/test/test_routes.py"
144+
instructions: "Create test file for routes"
145+
code_edit: "// the rest code"
143146
```
144147
</devin>
145148
现在,你可以执行代码
@@ -207,10 +210,11 @@ feat: add delete blog functionality
207210
</devin>
208211
// 其它代码修改
209212
<devin>
210-
/patch:SketchRunContext.java
211-
```patch
212-
Index: SketchRunContext.java
213-
...
213+
/edit_file:SketchRunContext.java
214+
```
215+
target_file: "SketchRunContext.java"
216+
instructions: "Optimize SketchRunContext code structure"
217+
code_edit: "..."
214218
```
215219
</devin>
216220
// 你需要根据上下文来生成启动命令,可以尽可能使用 bash 命令来启动应用程序
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package cc.unitmesh.devti.language.compiler.exec
2+
3+
import cc.unitmesh.devti.AutoDevNotifications
4+
import cc.unitmesh.devti.command.InsCommand
5+
import cc.unitmesh.devti.command.dataprovider.BuiltinCommand
6+
import cc.unitmesh.devti.language.compiler.error.DEVINS_ERROR
7+
import cc.unitmesh.devti.language.utils.lookupFile
8+
import cc.unitmesh.devti.sketch.AutoSketchMode
9+
import cc.unitmesh.devti.sketch.ui.patch.readText
10+
import cc.unitmesh.devti.sketch.ui.patch.writeText
11+
import com.intellij.openapi.application.runInEdt
12+
import com.intellij.openapi.application.runReadAction
13+
import com.intellij.openapi.fileEditor.FileEditorManager
14+
import com.intellij.openapi.project.Project
15+
import com.intellij.openapi.project.guessProjectDir
16+
import com.intellij.openapi.util.Disposer
17+
import com.intellij.openapi.vfs.VirtualFile
18+
19+
class EditFileInsCommand(val myProject: Project, val prop: String, val codeContent: String) : InsCommand {
20+
override val commandName: BuiltinCommand = BuiltinCommand.EDIT_FILE
21+
22+
override suspend fun execute(): String? {
23+
val editRequest = parseEditRequest(codeContent)
24+
if (editRequest == null) {
25+
val shouldShowNotification = shouldShowParseErrorNotification()
26+
if (shouldShowNotification) {
27+
AutoDevNotifications.warn(myProject, "Failed to parse edit_file request from content")
28+
}
29+
return "$DEVINS_ERROR: Failed to parse edit_file request"
30+
}
31+
32+
val disposable = Disposer.newCheckedDisposable()
33+
var result: String? = null
34+
35+
runInEdtAsync(disposable) {
36+
val projectDir = myProject.guessProjectDir()
37+
if (projectDir == null) {
38+
result = "$DEVINS_ERROR: Project directory not found"
39+
return@runInEdtAsync
40+
}
41+
42+
val targetFile = findTargetFile(editRequest.targetFile, projectDir)
43+
if (targetFile == null) {
44+
result = "$DEVINS_ERROR: File not found: ${editRequest.targetFile}"
45+
return@runInEdtAsync
46+
}
47+
48+
try {
49+
val originalContent = targetFile.readText()
50+
val editedContent = applyEdit(originalContent, editRequest.codeEdit)
51+
52+
targetFile.writeText(editedContent)
53+
FileEditorManager.getInstance(myProject).openFile(targetFile, true)
54+
55+
result = "File edited successfully: ${editRequest.targetFile}"
56+
} catch (e: Exception) {
57+
result = "$DEVINS_ERROR: Failed to apply edit to ${editRequest.targetFile}: ${e.message}"
58+
}
59+
}
60+
61+
if (AutoSketchMode.getInstance(myProject).isEnable) {
62+
result = ""
63+
}
64+
65+
return result
66+
}
67+
68+
private fun findTargetFile(targetPath: String, projectDir: VirtualFile): VirtualFile? {
69+
return runReadAction {
70+
// Try relative path first
71+
projectDir.findFileByRelativePath(targetPath)
72+
?: myProject.lookupFile(targetPath)
73+
}
74+
}
75+
76+
private fun applyEdit(originalContent: String, codeEdit: String): String {
77+
val originalLines = originalContent.lines()
78+
val editLines = codeEdit.lines()
79+
80+
// Simple approach: replace the entire content with the edit
81+
// The edit should contain the complete intended content with markers
82+
val result = mutableListOf<String>()
83+
var originalIndex = 0
84+
85+
for (editLine in editLines) {
86+
val trimmedEditLine = editLine.trim()
87+
88+
// Check if this is an "existing code" marker
89+
if (isExistingCodeMarker(trimmedEditLine)) {
90+
// Find the next non-marker line in the edit to know where to stop copying
91+
val nextEditLineIndex = findNextNonMarkerLine(editLines, editLines.indexOf(editLine) + 1)
92+
val nextEditLine = if (nextEditLineIndex >= 0) editLines[nextEditLineIndex].trim() else null
93+
94+
// Copy original lines until we find the next edit line or reach the end
95+
while (originalIndex < originalLines.size) {
96+
val originalLine = originalLines[originalIndex]
97+
result.add(originalLine)
98+
originalIndex++
99+
100+
// If we found the next edit line in the original, stop copying
101+
if (nextEditLine != null && originalLine.trim() == nextEditLine) {
102+
originalIndex-- // Back up one so the next edit line replaces this one
103+
break
104+
}
105+
}
106+
} else {
107+
// This is an actual edit line - add it and skip any matching original line
108+
result.add(editLine)
109+
110+
// Skip the corresponding original line if it matches
111+
if (originalIndex < originalLines.size &&
112+
originalLines[originalIndex].trim() == trimmedEditLine) {
113+
originalIndex++
114+
}
115+
}
116+
}
117+
118+
return result.joinToString("\n")
119+
}
120+
121+
private fun isExistingCodeMarker(line: String): Boolean {
122+
return line.startsWith("//") &&
123+
(line.contains("existing code") || line.contains("... existing code ..."))
124+
}
125+
126+
private fun findNextNonMarkerLine(lines: List<String>, startIndex: Int): Int {
127+
for (i in startIndex until lines.size) {
128+
if (!isExistingCodeMarker(lines[i].trim())) {
129+
return i
130+
}
131+
}
132+
return -1
133+
}
134+
135+
private fun parseEditRequest(content: String): EditRequest? {
136+
return try {
137+
// Parse the edit_file function call format - handle both JSON-like and function parameter formats
138+
val targetFileRegex = """target_file["\s]*[:=]["\s]*["']([^"']+)["']""".toRegex()
139+
val instructionsRegex = """instructions["\s]*[:=]["\s]*["']([^"']*?)["']""".toRegex(RegexOption.DOT_MATCHES_ALL)
140+
141+
// For code_edit, we need to handle multiline content more carefully
142+
val codeEditPattern = """code_edit["\s]*[:=]["\s]*["'](.*?)["']""".toRegex(RegexOption.DOT_MATCHES_ALL)
143+
144+
val targetFileMatch = targetFileRegex.find(content)
145+
val instructionsMatch = instructionsRegex.find(content)
146+
val codeEditMatch = codeEditPattern.find(content)
147+
148+
if (targetFileMatch != null && codeEditMatch != null) {
149+
val codeEditContent = codeEditMatch.groupValues[1]
150+
.replace("\\n", "\n") // Handle escaped newlines
151+
.replace("\\\"", "\"") // Handle escaped quotes
152+
.replace("\\'", "'") // Handle escaped single quotes
153+
154+
EditRequest(
155+
targetFile = targetFileMatch.groupValues[1],
156+
instructions = instructionsMatch?.groupValues?.get(1) ?: "",
157+
codeEdit = codeEditContent
158+
)
159+
} else {
160+
null
161+
}
162+
} catch (e: Exception) {
163+
null
164+
}
165+
}
166+
167+
private fun shouldShowParseErrorNotification(): Boolean {
168+
val currentTime = System.currentTimeMillis()
169+
if (currentTime - lastErrorTime > ERROR_TIME_WINDOW_MS) {
170+
parseErrorCount = 0
171+
lastErrorTime = currentTime
172+
}
173+
174+
parseErrorCount++
175+
176+
return parseErrorCount <= MAX_ERRORS_IN_WINDOW
177+
}
178+
179+
companion object {
180+
private const val MAX_ERRORS_IN_WINDOW = 3
181+
private const val ERROR_TIME_WINDOW_MS = 60000L
182+
private var parseErrorCount = 0
183+
private var lastErrorTime = 0L
184+
}
185+
}
186+
187+
data class EditRequest(
188+
val targetFile: String,
189+
val instructions: String,
190+
val codeEdit: String
191+
)

0 commit comments

Comments
 (0)