feat: improve failed diff error handling

This commit is contained in:
Carl-Robert Linnupuu 2025-05-31 16:09:44 +01:00
parent f9c9455f7c
commit bc0cd40d00
15 changed files with 301 additions and 131 deletions

View file

@ -69,7 +69,7 @@ public final class CompletionRequestService {
return getChatCompletion(request);
}
public EventSource getCodeEditsAsync(
public EventSource autoApplyAsync(
AutoApplyParameters params,
CompletionEventListener<String> eventListener) {
var request = CompletionRequestFactory

View file

@ -311,7 +311,7 @@ public class ChatMessageResponseBody extends JPanel {
prepareProcessingCode(searchReplace);
}
if (currentlyProcessedEditorPanel != null) {
currentlyProcessedEditorPanel.handleSearchReplace(searchReplace, partialResponse);
currentlyProcessedEditorPanel.handleSearchReplace(searchReplace);
handleHeaderOnCompletion(currentlyProcessedEditorPanel);
return;
}

View file

@ -3,6 +3,8 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import ee.carlrobert.codegpt.codecompletions.CompletionProgressNotifier
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.state.EditorStateManager
@ -14,7 +16,8 @@ import okhttp3.sse.EventSource
class AutoApplyListener(
private val project: Project,
private val stateManager: EditorStateManager,
private val onEditorReplaced: (EditorEx) -> Unit
private val virtualFile: VirtualFile,
private val onEditorReplaced: (EditorEx, EditorEx) -> Unit
) : CompletionEventListener<String> {
private val logger = logger<AutoApplyListener>()
@ -61,9 +64,22 @@ class AutoApplyListener(
if (!editorReplaced) {
editorReplaced = true
val newState = stateManager.createFromSegment(segment)
onEditorReplaced(newState.editor)
val oldEditor = stateManager.getCurrentState()?.editor ?: return
val currentText = virtualFile.readText()
val containsText = currentText.contains(segment.search.trim())
val newState = if (containsText) {
stateManager.createFromSegment(segment)
} else {
stateManager.transitionToFailedDiffState(
segment.search,
segment.replace,
virtualFile
) ?: return
}
onEditorReplaced(oldEditor, newState.editor)
}
handleReplace(segment)
}

View file

@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor
import com.intellij.diff.tools.fragmented.UnifiedDiffViewer
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.ScrollType
@ -13,8 +12,11 @@ import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.completions.AutoApplyParameters
import ee.carlrobert.codegpt.completions.CompletionRequestService
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffSyncManager
import ee.carlrobert.codegpt.toolwindow.chat.editor.factory.ComponentFactory
import ee.carlrobert.codegpt.toolwindow.chat.editor.factory.ComponentFactory.EXPANDED_KEY
@ -26,7 +28,7 @@ import ee.carlrobert.codegpt.toolwindow.chat.parser.SearchReplace
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
class ResponseEditorPanel(
project: Project,
private val project: Project,
item: Segment,
readOnly: Boolean,
disposableParent: Disposable,
@ -40,7 +42,7 @@ class ResponseEditorPanel(
val RESPONSE_EDITOR_STATE_KEY = Key.create<EditorState>("proxyai.responseEditorState")
}
private val stateManager = project.service<EditorStateManager>()
private val stateManager = EditorStateManager(project)
private var searchReplaceHandler: SearchReplaceHandler
init {
@ -60,35 +62,8 @@ class ResponseEditorPanel(
Disposer.register(disposableParent, this)
}
private fun configureEditor(editor: EditorEx) {
editor.document.addDocumentListener(object : BulkAwareDocumentListener.Simple {
override fun documentChanged(event: DocumentEvent) {
runInEdt {
updateEditorUI()
if (editor.editorKind != EditorKind.DIFF) {
scrollToEnd()
}
}
}
})
}
private fun updateEditorUI() {
updateEditorHeightAndUI()
updateExpandLinkVisibility()
}
override fun dispose() {
val state = stateManager.getCurrentState()
val editor = state?.editor ?: return
val filePath = state.segment.filePath
if (filePath != null) {
DiffSyncManager.unregisterEditor(filePath, editor)
}
}
fun handleSearchReplace(item: SearchReplace, partialResponse: Boolean) {
searchReplaceHandler.handleSearchReplace(item, partialResponse)
fun handleSearchReplace(item: SearchReplace) {
searchReplaceHandler.handleSearchReplace(item)
}
fun handleReplace(item: ReplaceWaiting) {
@ -117,10 +92,41 @@ class ResponseEditorPanel(
}
}
fun removeEditorAndAuxiliaryPanels() {
removeAll()
revalidate()
repaint()
fun applyCodeAsync(content: String, virtualFile: VirtualFile, editor: EditorEx) {
CompletionRequestService.getInstance().autoApplyAsync(
AutoApplyParameters(content, virtualFile),
AutoApplyListener(project, stateManager, virtualFile) { oldEditor, newEditor ->
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
?: throw IllegalStateException("Expected parent to be ResponseEditorPanel")
responseEditorPanel.replaceEditor(oldEditor, newEditor)
})
}
override fun dispose() {
val state = stateManager.getCurrentState()
val editor = state?.editor ?: return
val filePath = state.segment.filePath
if (filePath != null) {
DiffSyncManager.unregisterEditor(filePath, editor)
}
}
private fun configureEditor(editor: EditorEx) {
editor.document.addDocumentListener(object : BulkAwareDocumentListener.Simple {
override fun documentChanged(event: DocumentEvent) {
runInEdt {
updateEditorUI()
if (editor.editorKind != EditorKind.DIFF) {
scrollToEnd()
}
}
}
})
}
private fun updateEditorUI() {
updateEditorHeightAndUI()
updateExpandLinkVisibility()
}
private fun updateEditorHeightAndUI() {

View file

@ -1,6 +1,6 @@
package ee.carlrobert.codegpt.toolwindow.chat.editor
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.vfs.readText
@ -12,15 +12,19 @@ import ee.carlrobert.codegpt.toolwindow.chat.parser.Code
import ee.carlrobert.codegpt.toolwindow.chat.parser.ReplaceWaiting
import ee.carlrobert.codegpt.toolwindow.chat.parser.SearchReplace
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
import ee.carlrobert.codegpt.ui.OverlayUtil
class SearchReplaceHandler(
private val stateManager: EditorStateManager,
private val onEditorReplaced: (EditorEx, EditorEx) -> Unit
) {
companion object {
private val logger = thisLogger()
}
private var searchFailed = false
fun handleSearchReplace(item: SearchReplace, partialResponse: Boolean) {
fun handleSearchReplace(item: SearchReplace) {
val editor = stateManager.getCurrentState()?.editor ?: return
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.handleDone()
@ -59,7 +63,6 @@ class SearchReplaceHandler(
val containsText = currentText.contains(searchContent.trim())
if (searchContent.isNotEmpty() && editor.editorKind == EditorKind.DIFF && !containsText && !searchFailed) {
searchFailed = true
handleFailedDiffSearch(searchContent, replaceContent)
return
}
@ -68,25 +71,26 @@ class SearchReplaceHandler(
}
private fun handleNonExistentFile(replaceContent: String) {
logger.debug("Could not find file to replace in, falling back to untyped editor")
val state = stateManager.getCurrentState() ?: return
val oldEditor = state.editor
val segment = Code(replaceContent, state.segment.language, state.segment.filePath)
val newState = stateManager.createFromSegment(segment)
val newEditor = newState.editor
onEditorReplaced(oldEditor, newState.editor)
onEditorReplaced(oldEditor, newEditor)
searchFailed = true
}
private fun handleFailedDiffSearch(searchContent: String, replaceContent: String) {
logger.debug("Could not map diff search to file, falling back to untyped editor")
val oldEditor = stateManager.getCurrentState()?.editor ?: return
val newState = stateManager.transitionToFailedDiffState(searchContent, replaceContent)
if (newState != null) {
val newEditor = newState.editor
runInEdt {
onEditorReplaced(oldEditor, newEditor)
}
stateManager.transitionToFailedDiffState(searchContent, replaceContent)?.let {
onEditorReplaced(oldEditor, it.editor)
}
searchFailed = true
}
}

View file

@ -14,7 +14,6 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.util.concurrency.annotations.RequiresEdt
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffManagerUtil.replaceContent
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
class DiffEditorManager(
private val project: Project,
@ -29,7 +28,7 @@ class DiffEditorManager(
runInEdt {
document.replaceContent(
project,
currentText.replaceFirst(searchContent.trim(), replaceContent.trim())
currentText.replaceLast(searchContent.trim(), replaceContent.trim())
)
diffViewer.rediff(true)
@ -38,6 +37,12 @@ class DiffEditorManager(
return true
}
fun String.replaceLast(search: String, replacement: String): String {
val index = lastIndexOf(search)
return if (index < 0) this else
substring(0, index) + replacement + substring(index + search.length)
}
fun applyAllChanges(): List<UnifiedDiffChange> {
val document = diffViewer.getDocument(Side.LEFT)
DiffManagerUtil.ensureDocumentWritable(project, document)
@ -56,7 +61,9 @@ class DiffEditorManager(
DiffBundle.message("message.replace.change.command")
) {
diffViewer.replaceChange(change, Side.RIGHT)
diffViewer.scheduleRediff()
runInEdt {
diffViewer.scheduleRediff()
}
}
diffViewer.rediff(true)

View file

@ -3,22 +3,25 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor.factory
import com.intellij.diff.DiffContentFactory
import com.intellij.diff.requests.SimpleDiffRequest
import com.intellij.diff.tools.fragmented.UnifiedDiffViewer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.ContextMenuPopupHandler
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.ui.ColorUtil
import com.intellij.util.ui.JBUI
import com.intellij.vcsUtil.VcsUtil.getVirtualFile
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.predictions.CodeSuggestionDiffViewer.MyDiffContext
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffSyncManager
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.ToolWindowEditorFileDetails
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffSyncManager
import ee.carlrobert.codegpt.toolwindow.chat.parser.ReplaceWaiting
import ee.carlrobert.codegpt.toolwindow.chat.parser.SearchReplace
import ee.carlrobert.codegpt.toolwindow.chat.parser.SearchWaiting
@ -29,11 +32,9 @@ import javax.swing.JComponent
object EditorFactory {
fun createEditor(
project: Project,
segment: Segment,
readOnly: Boolean,
): EditorEx {
private val logger = thisLogger()
fun createEditor(project: Project, segment: Segment): EditorEx {
val content = segment.content
val languageMapping = FileUtil.findLanguageExtensionMapping(segment.language)
val isDiffType = isDiffType(segment, content)
@ -86,9 +87,15 @@ object EditorFactory {
}
private fun createDiffEditor(project: Project, segment: Segment): EditorEx? {
val filePath = segment.filePath ?: return null
val virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath)
?: return null
val filePath = segment.filePath
if (filePath == null) {
logger.warn("Cannot create diff editor for non-existent path")
return null
}
val virtualFile = ApplicationManager.getApplication().executeOnPooledThread<VirtualFile?> {
LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath)
}.get() ?: return null
val leftContent = DiffContentFactory.getInstance().create(project, virtualFile)
val rightContentDoc = EditorFactory.getInstance().createDocument(virtualFile.readText())

View file

@ -3,8 +3,8 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor.header
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.toolbarLayout.ToolbarLayoutStrategy
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.JBMenuItem
@ -15,8 +15,8 @@ import com.intellij.ui.AnimatedIcon
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.*
import ee.carlrobert.codegpt.toolwindow.chat.editor.state.EditorStateManager
import ee.carlrobert.codegpt.util.EditorUtil
import ee.carlrobert.codegpt.util.StringUtil
import javax.swing.JPanel
@ -43,12 +43,17 @@ class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
}
}
fun setLoading() {
setRightPanelComponent(loadingLabel)
fun setLoading(label: String = "Loading...") {
runInEdt {
loadingLabel.text = label
setRightPanelComponent(loadingLabel)
}
}
fun handleDone() {
setRightPanelComponent(createHeaderActions().component)
runInEdt {
setRightPanelComponent(createHeaderActions().component)
}
}
private fun createHeaderActions(): ActionToolbar {
@ -82,9 +87,10 @@ class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
return
}
setLoading()
project.service<EditorStateManager>()
.getCodeEditsAsync(editor.document.text, file, editor)
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
?: throw IllegalStateException("Could not find corresponding ResponseEditorPanel")
responseEditorPanel.applyCodeAsync(editor.document.text, file, editor)
setLoading("Editing...")
}
private fun createToolbar(actionGroup: ActionGroup): ActionToolbar {
@ -93,7 +99,6 @@ class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
toolbar.layoutStrategy = ToolbarLayoutStrategy.NOWRAP_STRATEGY
toolbar.targetComponent = this
toolbar.component.border = JBUI.Borders.empty()
toolbar.updateActionsAsync()
return toolbar
}

View file

@ -70,7 +70,7 @@ class DiffHeaderPanel(
runInEdt {
val container = config.editorEx.component.parent
if (container is ResponseEditorPanel) {
container.removeEditorAndAuxiliaryPanels()
container.removeAll()
container.add(diffAcceptedPanel, BorderLayout.CENTER)
container.revalidate()
container.repaint()

View file

@ -0,0 +1,99 @@
package ee.carlrobert.codegpt.toolwindow.chat.editor.header
import com.intellij.codeInsight.documentation.DocumentationHintEditorPane
import com.intellij.lang.documentation.DocumentationImageResolver
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.ui.popup.PopupFactoryImpl
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import ee.carlrobert.codegpt.CodeGPTBundle
import java.awt.Image
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.Timer
class ErrorPopoverHandler(
private val project: Project,
private val errorLabel: JComponent,
private val errorContent: String?
) {
private var errorPopup: JBPopup? = null
fun install() {
for (listener in errorLabel.mouseListeners) {
errorLabel.removeMouseListener(listener)
}
errorLabel.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
errorLabel.putClientProperty("mouseInside", true)
showErrorPopoverWithHover()
}
override fun mouseExited(e: MouseEvent) {
errorLabel.putClientProperty("mouseInside", false)
schedulePopupCloseIfNeeded()
}
})
}
private fun schedulePopupCloseIfNeeded() {
Timer(100) {
if (errorLabel.getClientProperty("mouseInside") != true &&
errorLabel.getClientProperty("popupMouseInside") != true
) {
errorPopup?.cancel()
errorPopup = null
}
}.apply { isRepeats = false }.start()
}
private fun showErrorPopoverWithHover() {
if (errorContent == null) return
if (errorPopup != null && errorPopup!!.isVisible) return
val documentationHint = DocumentationHintEditorPane(
project,
emptyMap(),
object : DocumentationImageResolver {
override fun resolveImage(url: String): Image? = null
}
).apply {
setText(errorContent)
isEditable = false
isOpaque = true
border = JBUI.Borders.emptyTop(10)
foreground = UIUtil.getToolTipForeground()
background = UIUtil.getToolTipActionBackground()
font = UIUtil.getToolTipFont()
}
val popup = PopupFactoryImpl.getInstance()
.createComponentPopupBuilder(documentationHint, null)
.setRequestFocus(false)
.setResizable(true)
.setMovable(true)
.setTitle(CodeGPTBundle.get("headerPanel.error.searchBlockNotMapped.title"))
.setShowShadow(true)
.setCancelOnClickOutside(true)
.createPopup()
documentationHint.setHint(popup)
documentationHint.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
errorLabel.putClientProperty("popupMouseInside", true)
}
override fun mouseExited(e: MouseEvent) {
errorLabel.putClientProperty("popupMouseInside", false)
schedulePopupCloseIfNeeded()
}
})
errorPopup = popup
popup.showUnderneathOf(errorLabel)
}
}

View file

@ -19,10 +19,11 @@ import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffStatsComponent
import ee.carlrobert.codegpt.util.file.FileUtil
import org.jetbrains.kotlin.idea.projectView.KotlinSelectInProjectViewProvider
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import java.io.File
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
@ -33,6 +34,7 @@ data class HeaderConfig(
val filePath: String?,
val language: String,
val readOnly: Boolean,
val error: String? = null,
val loading: Boolean = false
)
@ -45,15 +47,18 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
private val statsComponent = SimpleColoredComponent().apply {
font = JBUI.Fonts.smallFont()
}
protected var virtualFile: VirtualFile? = FileUtil.resolveVirtualFile(config.filePath)
private val errorLabel = JBLabel(AllIcons.General.Error).apply {
isVisible = config.error != null
border = JBUI.Borders.emptyRight(4)
}
private val rightPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
alignmentY = 0.5f
isOpaque = false
}
protected var virtualFile: VirtualFile? = FileUtil.resolveVirtualFile(config.filePath)
protected abstract fun initializeRightPanel(rightPanel: JPanel)
fun updateDiffStats(changes: List<UnifiedDiffChange>) {
@ -66,7 +71,7 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
protected fun setupUI() {
setupPanelAppearance()
setupFileLinkOrLanguageLabel(virtualFile)
addToLeft(createLeftPanel(virtualFile))
rightPanel.removeAll()
initializeRightPanel(rightPanel)
@ -103,13 +108,26 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
minimumSize = Dimension(preferredSize.width, 32)
}
private fun setupFileLinkOrLanguageLabel(virtualFile: VirtualFile?) {
private fun createLeftPanel(virtualFile: VirtualFile?): JComponent {
val filePath = config.filePath
when {
filePath == null -> addToLeft(createLanguageLabel())
virtualFile == null -> addToLeft(createNewFileLink(filePath, config.editorEx))
else -> addToLeft(createFileLinkPanel(virtualFile))
val linkOrLabel = when {
filePath == null -> createLanguageLabel()
virtualFile == null -> createNewFileLink(filePath, config.editorEx)
else -> createFileLinkPanel(virtualFile)
}
if (config.error != null) {
errorLabel.isVisible = true
errorLabel.cursor = java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR)
ErrorPopoverHandler(config.project, errorLabel, config.error).install()
return JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply {
isOpaque = false
add(errorLabel)
add(linkOrLabel)
}
}
return linkOrLabel
}
private fun createFileLinkPanel(virtualFile: VirtualFile): JPanel {
@ -152,7 +170,7 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
}
remove(actionLink)
setupFileLinkOrLanguageLabel(newFile)
addToLeft(createLeftPanel(newFile))
OpenFileAction.openFile(newFile, config.project)
ProjectView.getInstance(config.project).select(null, newFile, true)
@ -165,7 +183,6 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
private fun createLanguageLabel(): JBLabel {
return JBLabel(config.language).apply {
foreground = JBColor.GRAY
border = JBUI.Borders.emptyLeft(4)
}
}
}

View file

@ -1,30 +1,25 @@
package ee.carlrobert.codegpt.toolwindow.chat.editor.state
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.completions.AutoApplyParameters
import ee.carlrobert.codegpt.completions.CompletionRequestService
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel.Companion.RESPONSE_EDITOR_STATE_KEY
import ee.carlrobert.codegpt.toolwindow.chat.editor.AutoApplyListener
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffEditorManager
import ee.carlrobert.codegpt.toolwindow.chat.editor.factory.EditorFactory
import ee.carlrobert.codegpt.toolwindow.chat.parser.Code
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
@Service(Service.Level.PROJECT)
class EditorStateManager(private val project: Project) {
private var currentState: EditorState? = null
private var diffEditorManager: DiffEditorManager? = null
fun createFromSegment(segment: Segment, readOnly: Boolean = false): EditorState {
val editor = EditorFactory.createEditor(project, segment, readOnly)
val editor = EditorFactory.createEditor(project, segment)
val state = if (editor.editorKind == EditorKind.DIFF) {
createDiffState(editor, segment)
} else {
@ -39,29 +34,22 @@ class EditorStateManager(private val project: Project) {
return state
}
fun getCodeEditsAsync(
content: String,
virtualFile: VirtualFile,
editor: EditorEx,
) {
val params = AutoApplyParameters(content, virtualFile)
val listener = AutoApplyListener(project, this) { newEditor ->
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
?: throw IllegalStateException("Expected parent to be ResponseEditorPanel")
responseEditorPanel.replaceEditor(editor, newEditor)
}
CompletionRequestService.getInstance().getCodeEditsAsync(params, listener)
}
fun transitionToFailedDiffState(searchContent: String, replaceContent: String): EditorState? {
val currentState = this.currentState ?: return null
val segment = currentState.segment
val virtualFile = getVirtualFile(segment.filePath) ?: return null
return transitionToFailedDiffState(searchContent, replaceContent, virtualFile)
}
fun transitionToFailedDiffState(
searchContent: String,
replaceContent: String,
virtualFile: VirtualFile
): EditorState? {
val newSegment = Code(replaceContent, virtualFile.extension ?: "Text", virtualFile.path)
val newEditor = EditorFactory.createEditor(project, newSegment, false)
val newEditor = EditorFactory.createEditor(project, newSegment)
val newState =
FailedDiffEditorState(newEditor, newSegment, project, searchContent, replaceContent)

View file

@ -4,6 +4,7 @@ import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DefaultHeaderPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.HeaderConfig
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
@ -29,6 +30,37 @@ class FailedDiffEditorState(
val filePath = segment.filePath
val extension = filePath?.substringAfterLast('.', "txt") ?: "txt"
return DefaultHeaderPanel(HeaderConfig(project, editor, filePath, extension, false))
val content = """
<html>
<body>
<div class='content'>
<p>The model-generated <code>SEARCH</code> block could not be mapped to any existing code in your file.</p>
<div style="margin-bottom: 4px;"><b>Failed search block:</b></div>
<div class="code-block">
<pre style="padding: 0px; margin: 0px"><span style="">$searchContent</span></pre>
</div>
<p>
Tips to consider:
</p>
<ul style="padding-left: 4px;">
<li>Use the best performing model available to you.</li>
<li>Keep the context window small - avoid large files and conversations.</li>
<li>Give clear, step-by-step instructions for your request.</li>
<li>Specify which updates are already applied and which are outstanding.</li>
</ul>
</div>
<table class='sections'>
<tr>
<td valign='top' class='section'><p>See also:</td>
<td valign='top'>
<p><a href="https://docs.tryproxy.io/#multi-file-edits">https://docs.tryproxy.io/#multi-file-edits</a></p>
</td>
</table>
</body>
</html>
""".trimIndent()
return DefaultHeaderPanel(HeaderConfig(project, editor, filePath, extension, false, content))
}
}

View file

@ -6,7 +6,6 @@ import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DefaultHeaderPanel
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.HeaderConfig
import ee.carlrobert.codegpt.toolwindow.chat.parser.Code
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
import ee.carlrobert.codegpt.util.file.FileUtil
import javax.swing.JComponent
@ -27,18 +26,14 @@ class RegularEditorState(
override fun createHeaderComponent(readOnly: Boolean): JComponent? {
val languageMapping = FileUtil.findLanguageExtensionMapping(segment.language)
return if (segment is Code) {
DefaultHeaderPanel(
HeaderConfig(
project,
editor,
segment.filePath,
languageMapping.key,
readOnly
),
)
} else {
null
}
return DefaultHeaderPanel(
HeaderConfig(
project,
editor,
segment.filePath,
languageMapping.key,
readOnly
),
)
}
}

View file

@ -320,10 +320,4 @@ tagPopupMenuItem.closeAll=Close All Tags
tagPopupMenuItem.closeTagsToLeft=Close Tags to the Left
tagPopupMenuItem.closeTagsToRight=Close Tags to the Right
toolwindow.chat.loading=Generating response...
toolwindow.chat.editor.action.autoApply.dialog.title=File Not Found
toolwindow.chat.editor.action.autoApply.dialog.message=Oops! The file can't be found. How do you want to handle this?
toolwindow.chat.editor.action.autoApply.dialog.createNew=Create New File
toolwindow.chat.editor.action.autoApply.dialog.applyToOpenFile=Apply to Open File
toolwindow.chat.editor.action.autoApply.dialog.cancel=Cancel
toolwindow.chat.editor.action.autoApply.creatingFile=Creating new file...
toolwindow.chat.editor.action.autoApply.fileCreationError=Error creating file: {0}
headerPanel.error.searchBlockNotMapped.title=Failed to Locate Search Block