Skip to content

Commit 5206cce

Browse files
committed
refactor(devti): implement diff viewer for AI suggestions #352
- Add SimpleDiffViewer to display differences between original and AI-generated code - Implement ChangeActionListener to handle accept/cancel actions in diff viewer - Update PlannerResultSummary to use new diff viewer functionality - Refactor AgentStateService to create Change objects instead of ShelvedChange - Improve patch application and conflict handling in AgentStateService
1 parent 3369070 commit 5206cce

File tree

2 files changed

+147
-25
lines changed

2 files changed

+147
-25
lines changed

core/src/main/kotlin/cc/unitmesh/devti/gui/planner/PlannerResultSummary.kt

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ package cc.unitmesh.devti.gui.planner
33
import cc.unitmesh.devti.AutoDevBundle
44
import cc.unitmesh.devti.AutoDevIcons
55
import cc.unitmesh.devti.util.relativePath
6+
import com.intellij.diff.DiffContentFactoryEx
7+
import com.intellij.diff.DiffContext
8+
import com.intellij.diff.requests.SimpleDiffRequest
9+
import com.intellij.diff.tools.simple.SimpleDiffViewer
610
import com.intellij.icons.AllIcons
711
import com.intellij.openapi.actionSystem.AnAction
812
import com.intellij.openapi.actionSystem.AnActionEvent
9-
import com.intellij.openapi.fileEditor.FileEditorManager
13+
import com.intellij.openapi.application.runInEdt
14+
import com.intellij.openapi.fileEditor.FileDocumentManager
1015
import com.intellij.openapi.project.Project
16+
import com.intellij.openapi.ui.DialogWrapper
1117
import com.intellij.openapi.vcs.changes.Change
1218
import com.intellij.openapi.vcs.changes.ui.RollbackWorker
1319
import com.intellij.ui.HyperlinkLabel
@@ -48,15 +54,49 @@ class PlannerResultSummary(
4854
}
4955

5056
override fun onAcceptAll() {
51-
57+
changes.forEach { change ->
58+
changeActionListener.onAccept(change)
59+
}
5260
}
5361
}
5462

55-
5663
private var changeActionListener: ChangeActionListener = object : ChangeActionListener {
5764
override fun onView(change: Change) {
5865
change.virtualFile?.also {
59-
FileEditorManager.getInstance(project).openFile(it, true)
66+
val diffFactory = DiffContentFactoryEx.getInstanceEx()
67+
val oldCode = change.beforeRevision?.content ?: return
68+
val newCode = change.afterRevision?.content ?: return
69+
val currentDocContent = diffFactory.create(project, oldCode)
70+
val newDocContent = diffFactory.create(newCode)
71+
72+
val diffRequest =
73+
SimpleDiffRequest("Diff", currentDocContent, newDocContent, "Original", "AI suggestion")
74+
75+
val diffViewer = SimpleDiffViewer(object : DiffContext() {
76+
override fun getProject() = this@PlannerResultSummary.project
77+
override fun isWindowFocused() = false
78+
override fun isFocusedInWindow() = false
79+
override fun requestFocusInWindow() = Unit
80+
}, diffRequest)
81+
diffViewer.init()
82+
83+
val dialog = object : DialogWrapper(project) {
84+
init {
85+
init()
86+
title = "Diff Viewer"
87+
}
88+
89+
override fun createCenterPanel(): JComponent = diffViewer.component
90+
override fun doOKAction() {
91+
super.doOKAction()
92+
changeActionListener.onAccept(change)
93+
}
94+
override fun doCancelAction() {
95+
super.doCancelAction()
96+
}
97+
}
98+
99+
dialog.show()
60100
}
61101
}
62102

@@ -67,7 +107,17 @@ class PlannerResultSummary(
67107
updateChanges(newChanges)
68108
}
69109

70-
override fun onAccept(change: Change) {}
110+
override fun onAccept(change: Change) {
111+
val file = change.virtualFile ?: return
112+
val content = change.afterRevision?.content ?: change.beforeRevision?.content
113+
114+
runInEdt {
115+
if (content != null) {
116+
val document = FileDocumentManager.getInstance().getDocument(file)
117+
document?.setText(content)
118+
}
119+
}
120+
}
71121
}
72122

73123
init {

core/src/main/kotlin/cc/unitmesh/devti/observer/agent/AgentStateService.kt

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,30 @@ import cc.unitmesh.devti.observer.plan.PlanUpdateListener
1010
import cc.unitmesh.devti.settings.AutoDevSettingsState
1111
import cc.unitmesh.devti.util.parser.MarkdownCodeHelper
1212
import com.intellij.openapi.application.ApplicationManager
13+
import com.intellij.openapi.application.ReadAction
1314
import com.intellij.openapi.components.Service
1415
import com.intellij.openapi.diagnostic.logger
15-
import com.intellij.openapi.diff.impl.patch.FilePatch
16+
import com.intellij.openapi.diff.impl.patch.TextFilePatch
17+
import com.intellij.openapi.diff.impl.patch.apply.GenericPatchApplier
18+
import com.intellij.openapi.fileEditor.FileDocumentManager
1619
import com.intellij.openapi.project.Project
20+
import com.intellij.openapi.util.ThrowableComputable
21+
import com.intellij.openapi.vcs.FilePath
1722
import com.intellij.openapi.vcs.FileStatus
18-
import com.intellij.openapi.vcs.changes.shelf.ShelvedChange
23+
import com.intellij.openapi.vcs.VcsBundle
24+
import com.intellij.openapi.vcs.VcsException
25+
import com.intellij.openapi.vcs.changes.Change
26+
import com.intellij.openapi.vcs.changes.ContentRevision
27+
import com.intellij.openapi.vcs.changes.CurrentContentRevision
28+
import com.intellij.openapi.vcs.changes.TextRevisionNumber
29+
import com.intellij.openapi.vcs.history.VcsRevisionNumber
30+
import com.intellij.openapi.vfs.VirtualFile
31+
import com.intellij.vcsUtil.VcsUtil
32+
import org.jetbrains.annotations.NonNls
33+
import java.io.File
34+
import java.io.IOException
1935
import java.nio.file.Path
36+
import java.util.*
2037

2138
@Service(Service.Level.PROJECT)
2239
class AgentStateService(val project: Project) {
@@ -34,34 +51,89 @@ class AgentStateService(val project: Project) {
3451
logger<AgentStateService>().info("Called agent tools:\n ${state.usedTools.joinToString("\n")}")
3552
}
3653

37-
fun addToChange(path: Path, change: FilePatch) {
38-
val shelvedChange = createShelvedChange(project, path, change)
39-
if (shelvedChange != null) {
40-
state.changes.add(shelvedChange.change)
54+
fun addToChange(path: Path, patch: TextFilePatch) {
55+
val change = createChange(patch)
56+
state.changes.add(change)
4157

42-
ApplicationManager.getApplication().messageBus
43-
.syncPublisher(PlanUpdateListener.TOPIC)
44-
.onUpdateChange(state.changes)
45-
}
58+
ApplicationManager.getApplication().messageBus
59+
.syncPublisher(PlanUpdateListener.TOPIC)
60+
.onUpdateChange(state.changes)
4661
}
4762

48-
fun createShelvedChange(project: Project, patchPath: Path, patch: FilePatch): ShelvedChange? {
49-
val beforeName: String? = patch.beforeName
50-
val afterName: String? = patch.afterName
51-
if (beforeName == null || afterName == null) {
52-
logger<AgentStateService>().warn("Failed to parse the file patch: [$patchPath]:$patch")
53-
return null
54-
}
63+
private fun createChange(patch: TextFilePatch): Change {
64+
val baseDir = File(project.basePath!!)
65+
val beforePath = patch.beforeName
66+
val afterPath = patch.afterName
5567

56-
val status = if (patch.isNewFile) {
68+
val fileStatus = if (patch.isNewFile) {
5769
FileStatus.ADDED
5870
} else if (patch.isDeletedFile) {
5971
FileStatus.DELETED
6072
} else {
6173
FileStatus.MODIFIED
6274
}
6375

64-
return ShelvedChange.create(project, patchPath, beforeName, afterName, status)
76+
val beforeFilePath = VcsUtil.getFilePath(getAbsolutePath(baseDir, beforePath), false)
77+
val afterFilePath = VcsUtil.getFilePath(getAbsolutePath(baseDir, afterPath), false)
78+
79+
var beforeRevision: ContentRevision? = null
80+
if (fileStatus !== FileStatus.ADDED) {
81+
beforeRevision = object : CurrentContentRevision(beforeFilePath) {
82+
override fun getRevisionNumber(): VcsRevisionNumber {
83+
return TextRevisionNumber(VcsBundle.message("local.version.title"))
84+
}
85+
}
86+
}
87+
88+
var afterRevision: ContentRevision? = null
89+
if (fileStatus !== FileStatus.DELETED) {
90+
afterRevision = object : CurrentContentRevision(beforeFilePath) {
91+
override fun getRevisionNumber(): VcsRevisionNumber =
92+
TextRevisionNumber(VcsBundle.message("local.version.title"))
93+
94+
override fun getVirtualFile(): VirtualFile? = afterFilePath.virtualFile
95+
override fun getFile(): FilePath = afterFilePath
96+
override fun getContent(): @NonNls String? {
97+
if (patch.isNewFile) {
98+
return patch.singleHunkPatchText
99+
}
100+
if (patch.isDeletedFile) {
101+
return null
102+
}
103+
104+
val localContent: String = loadLocalContent()
105+
val appliedPatch = GenericPatchApplier.apply(localContent, patch.getHunks())
106+
if (appliedPatch != null) {
107+
return appliedPatch.patchedText
108+
}
109+
throw VcsException(VcsBundle.message("patch.apply.error.conflict"))
110+
}
111+
112+
@Throws(VcsException::class)
113+
private fun loadLocalContent(): String {
114+
return ReadAction.compute<String?, VcsException?>(ThrowableComputable {
115+
val file: VirtualFile? = beforeFilePath.virtualFile
116+
if (file == null) throw VcsException("File $beforeFilePath not found")
117+
val doc = FileDocumentManager.getInstance().getDocument(file)
118+
if (doc == null) throw VcsException("Document $file not found")
119+
doc.text
120+
})
121+
}
122+
}
123+
}
124+
return Change(beforeRevision, afterRevision, fileStatus)
125+
}
126+
127+
private fun getAbsolutePath(baseDir: File, relativePath: String): File {
128+
var file: File?
129+
try {
130+
file = File(baseDir, relativePath).getCanonicalFile()
131+
} catch (e: IOException) {
132+
logger<AgentStateService>().info(e)
133+
file = File(baseDir, relativePath)
134+
}
135+
136+
return file
65137
}
66138

67139
fun buildOriginIntention(): String? {
@@ -119,4 +191,4 @@ class AgentStateService(val project: Project) {
119191
fun getPlan(): MutableList<AgentTaskEntry> {
120192
return state.plan
121193
}
122-
}
194+
}

0 commit comments

Comments
 (0)