diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index e2f12052..bfe22806 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -69,7 +69,7 @@ public final class CompletionRequestService { return getChatCompletion(request); } - public EventSource getCodeEditsAsync( + public EventSource autoApplyAsync( AutoApplyParameters params, CompletionEventListener eventListener) { var request = CompletionRequestFactory diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java index b8cd21f6..e9ec4e82 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java @@ -311,7 +311,7 @@ public class ChatMessageResponseBody extends JPanel { prepareProcessingCode(searchReplace); } if (currentlyProcessedEditorPanel != null) { - currentlyProcessedEditorPanel.handleSearchReplace(searchReplace, partialResponse); + currentlyProcessedEditorPanel.handleSearchReplace(searchReplace); handleHeaderOnCompletion(currentlyProcessedEditorPanel); return; } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/AutoApplyListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/AutoApplyListener.kt index 5d7872e1..8e041e38 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/AutoApplyListener.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/AutoApplyListener.kt @@ -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 { private val logger = logger() @@ -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) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.kt index 26f2c5b9..c2ab63e0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.kt @@ -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("proxyai.responseEditorState") } - private val stateManager = project.service() + 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() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/SearchReplaceHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/SearchReplaceHandler.kt index f8f1b041..9b7d997b 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/SearchReplaceHandler.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/SearchReplaceHandler.kt @@ -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 } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffEditorManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffEditorManager.kt index 83d34204..b40e115a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffEditorManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffEditorManager.kt @@ -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 { 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) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/factory/EditorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/factory/EditorFactory.kt index 2cc3cfbe..52dd2f70 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/factory/EditorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/factory/EditorFactory.kt @@ -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 { + LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath) + }.get() ?: return null val leftContent = DiffContentFactory.getInstance().create(project, virtualFile) val rightContentDoc = EditorFactory.getInstance().createDocument(virtualFile.readText()) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DefaultHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DefaultHeaderPanel.kt index 9d22cd22..8af272e7 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DefaultHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DefaultHeaderPanel.kt @@ -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() - .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 } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DiffHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DiffHeaderPanel.kt index fc742c25..c1584913 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DiffHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/DiffHeaderPanel.kt @@ -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() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/ErrorPopoverHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/ErrorPopoverHandler.kt new file mode 100644 index 00000000..0d23a73e --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/ErrorPopoverHandler.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/HeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/HeaderPanel.kt index 08e2e50b..3b882c4c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/HeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/header/HeaderPanel.kt @@ -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) { @@ -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) } } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/EditorStateManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/EditorStateManager.kt index 5bebcb8a..76980d17 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/EditorStateManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/EditorStateManager.kt @@ -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) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/FailedDiffEditorState.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/FailedDiffEditorState.kt index c92d3428..d2b66297 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/FailedDiffEditorState.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/FailedDiffEditorState.kt @@ -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 = """ + + +
+

The model-generated SEARCH block could not be mapped to any existing code in your file.

+ +
Failed search block:
+
+
$searchContent
+
+

+ Tips to consider: +

+
    +
  • Use the best performing model available to you.
  • +
  • Keep the context window small - avoid large files and conversations.
  • +
  • Give clear, step-by-step instructions for your request.
  • +
  • Specify which updates are already applied and which are outstanding.
  • +
+
+ + + + +

See also:

+

https://docs.tryproxy.io/#multi-file-edits

+
+ + + """.trimIndent() + + return DefaultHeaderPanel(HeaderConfig(project, editor, filePath, extension, false, content)) } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/RegularEditorState.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/RegularEditorState.kt index 6dc1adf5..20271b53 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/RegularEditorState.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/state/RegularEditorState.kt @@ -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 + ), + ) } } diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 6a058c77..2ace4911 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -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 \ No newline at end of file