mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-20 17:52:23 +00:00
feat: improve failed diff error handling
This commit is contained in:
parent
f9c9455f7c
commit
bc0cd40d00
15 changed files with 301 additions and 131 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ public class ChatMessageResponseBody extends JPanel {
|
|||
prepareProcessingCode(searchReplace);
|
||||
}
|
||||
if (currentlyProcessedEditorPanel != null) {
|
||||
currentlyProcessedEditorPanel.handleSearchReplace(searchReplace, partialResponse);
|
||||
currentlyProcessedEditorPanel.handleSearchReplace(searchReplace);
|
||||
handleHeaderOnCompletion(currentlyProcessedEditorPanel);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue