mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-22 11:38:28 +00:00
feat: support cancelling auto apply requests
This commit is contained in:
parent
9e2226188b
commit
ff03d9c1fb
12 changed files with 284 additions and 98 deletions
|
|
@ -17,23 +17,29 @@ class AutoApplyListener(
|
|||
private val project: Project,
|
||||
private val stateManager: EditorStateManager,
|
||||
private val virtualFile: VirtualFile,
|
||||
private val originalSuggestion: String,
|
||||
private val onEditorReplaced: (EditorEx, EditorEx) -> Unit
|
||||
) : CompletionEventListener<String> {
|
||||
|
||||
private val logger = logger<AutoApplyListener>()
|
||||
private var editorReplaced: Boolean = false
|
||||
private val messageParser = SseMessageParser()
|
||||
private var eventSource: EventSource? = null
|
||||
|
||||
override fun onOpen() {
|
||||
CompletionProgressNotifier.update(project, true)
|
||||
}
|
||||
|
||||
override fun onMessage(message: String, eventSource: EventSource?) {
|
||||
processMessageSegments(message, eventSource)
|
||||
if (this.eventSource == null && eventSource != null) {
|
||||
this.eventSource = eventSource
|
||||
}
|
||||
processMessageSegments(message)
|
||||
}
|
||||
|
||||
override fun onError(error: ErrorDetails?, ex: Throwable?) {
|
||||
logger.error("Something went wrong while retrying diff-based editing", ex)
|
||||
logger.error("Something went wrong while applying the changes", ex)
|
||||
ErrorHandler.handleError(error, ex)
|
||||
handleComplete()
|
||||
}
|
||||
|
||||
|
|
@ -45,10 +51,7 @@ class AutoApplyListener(
|
|||
handleComplete()
|
||||
}
|
||||
|
||||
private fun processMessageSegments(
|
||||
message: String,
|
||||
eventSource: EventSource?
|
||||
) {
|
||||
private fun processMessageSegments(message: String) {
|
||||
val segments = messageParser.parse(message)
|
||||
for (segment in segments) {
|
||||
when (segment) {
|
||||
|
|
@ -85,7 +88,7 @@ class AutoApplyListener(
|
|||
val containsText = currentText.contains(segment.search.trim())
|
||||
|
||||
val newState = if (containsText) {
|
||||
stateManager.createFromSegment(segment)
|
||||
stateManager.createFromSegment(segment, false, eventSource, originalSuggestion)
|
||||
} else {
|
||||
stateManager.transitionToFailedDiffState(
|
||||
segment.search,
|
||||
|
|
@ -101,5 +104,7 @@ class AutoApplyListener(
|
|||
val editor = stateManager.getCurrentState()?.editor ?: return
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.handleDone()
|
||||
CompletionProgressNotifier.update(project, false)
|
||||
eventSource = null
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat.editor
|
||||
|
||||
import com.intellij.notification.NotificationType
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import ee.carlrobert.llm.client.openai.completion.ErrorDetails
|
||||
|
||||
object ErrorHandler {
|
||||
|
||||
fun handleError(error: ErrorDetails?, ex: Throwable?) {
|
||||
val errorMessage = formatErrorMessage(error, ex)
|
||||
OverlayUtil.showNotification(errorMessage, NotificationType.ERROR)
|
||||
}
|
||||
|
||||
fun formatErrorMessage(error: ErrorDetails?, ex: Throwable?): String {
|
||||
return when {
|
||||
error?.code == "insufficient_quota" -> "You exceeded your current quota, please check your plan and billing details."
|
||||
ex?.message != null -> "Error: ${ex.message}"
|
||||
else -> "An unknown error occurred while applying changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor
|
|||
|
||||
import com.intellij.diff.tools.fragmented.UnifiedDiffViewer
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.EditorKind
|
||||
import com.intellij.openapi.editor.LogicalPosition
|
||||
import com.intellij.openapi.editor.ScrollType
|
||||
|
|
@ -10,11 +12,9 @@ import com.intellij.openapi.editor.event.BulkAwareDocumentListener
|
|||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.vfs.readText
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
|
|
@ -25,6 +25,7 @@ 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
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.factory.ComponentFactory.MIN_LINES_FOR_EXPAND
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DefaultHeaderPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.state.EditorState
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.state.EditorStateManager
|
||||
|
|
@ -98,17 +99,39 @@ class ResponseEditorPanel(
|
|||
}
|
||||
}
|
||||
|
||||
fun applyCodeAsync(content: String, virtualFile: VirtualFile, editor: EditorEx) {
|
||||
CompletionRequestService.getInstance().autoApplyAsync(
|
||||
fun replaceEditorWithSegment(segment: Segment) {
|
||||
val oldEditor = stateManager.getCurrentState()?.editor ?: return
|
||||
val newState = stateManager.createFromSegment(segment)
|
||||
replaceEditor(oldEditor, newState.editor)
|
||||
}
|
||||
|
||||
fun removeCurrentEditor() {
|
||||
runInEdt {
|
||||
removeAll()
|
||||
stateManager.clearCurrentState()
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
fun applyCodeAsync(content: String, virtualFile: VirtualFile, editor: EditorEx, headerPanel: DefaultHeaderPanel? = null) {
|
||||
val eventSource = CompletionRequestService.getInstance().autoApplyAsync(
|
||||
AutoApplyParameters(content, virtualFile),
|
||||
AutoApplyListener(project, stateManager, virtualFile) { oldEditor, newEditor ->
|
||||
AutoApplyListener(project, stateManager, virtualFile, content) { oldEditor, newEditor ->
|
||||
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
|
||||
?: throw IllegalStateException("Expected parent to be ResponseEditorPanel")
|
||||
responseEditorPanel.replaceEditor(oldEditor, newEditor)
|
||||
})
|
||||
|
||||
val panel = headerPanel ?: (editor.permanentHeaderComponent as? DefaultHeaderPanel)
|
||||
panel?.setLoading(eventSource)
|
||||
}
|
||||
|
||||
internal fun createReplaceWaitingSegment(searchContent: String, replaceContent: String, virtualFile: VirtualFile): ReplaceWaiting {
|
||||
|
||||
internal fun createReplaceWaitingSegment(
|
||||
searchContent: String,
|
||||
replaceContent: String,
|
||||
virtualFile: VirtualFile
|
||||
): ReplaceWaiting {
|
||||
return ReplaceWaiting(
|
||||
search = searchContent,
|
||||
replace = replaceContent,
|
||||
|
|
@ -116,26 +139,30 @@ class ResponseEditorPanel(
|
|||
filePath = virtualFile.path
|
||||
)
|
||||
}
|
||||
|
||||
fun createDiffEditorForDirectApply(searchContent: String, replaceContent: String, virtualFile: VirtualFile) {
|
||||
|
||||
fun createDiffEditorForDirectApply(
|
||||
searchContent: String,
|
||||
replaceContent: String,
|
||||
virtualFile: VirtualFile
|
||||
) {
|
||||
try {
|
||||
val segment = createReplaceWaitingSegment(searchContent, "", virtualFile)
|
||||
|
||||
|
||||
val oldEditor = stateManager.getCurrentState()?.editor
|
||||
if (oldEditor == null) {
|
||||
logger.warn("No current editor state found for direct apply")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val currentText = try {
|
||||
virtualFile.readText()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to read file content for direct apply", e)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val containsText = currentText.contains(segment.search.trim())
|
||||
|
||||
|
||||
val newState = if (containsText) {
|
||||
stateManager.createFromSegment(segment)
|
||||
} else {
|
||||
|
|
@ -148,15 +175,16 @@ class ResponseEditorPanel(
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
replaceEditor(oldEditor, newState.editor)
|
||||
|
||||
val finalSegment = createReplaceWaitingSegment(searchContent, replaceContent, virtualFile)
|
||||
|
||||
val finalSegment =
|
||||
createReplaceWaitingSegment(searchContent, replaceContent, virtualFile)
|
||||
newState.updateContent(finalSegment)
|
||||
|
||||
|
||||
val currentEditor = newState.editor
|
||||
val headerPanel = currentEditor.permanentHeaderComponent as? DiffHeaderPanel
|
||||
|
||||
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
if (!project.isDisposed) {
|
||||
headerPanel?.handleDone()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.intellij.openapi.editor.EditorKind
|
|||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.vfs.readText
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.codecompletions.CompletionProgressNotifier
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel.Companion.RESPONSE_EDITOR_DIFF_VIEWER_VALUE_PAIR_KEY
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.state.EditorStateManager
|
||||
|
|
@ -25,11 +26,11 @@ class SearchReplaceHandler(
|
|||
private var searchFailed = false
|
||||
|
||||
fun handleSearchReplace(item: SearchReplace) {
|
||||
handleReplace(item, item.filePath, item.search, item.replace)
|
||||
|
||||
val editor = stateManager.getCurrentState()?.editor ?: return
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.handleDone()
|
||||
|
||||
RESPONSE_EDITOR_DIFF_VIEWER_VALUE_PAIR_KEY.set(editor, Pair(item.search, item.replace))
|
||||
handleReplace(item, item.filePath, item.search, item.replace)
|
||||
}
|
||||
|
||||
fun handleReplace(item: ReplaceWaiting) {
|
||||
|
|
|
|||
|
|
@ -4,29 +4,32 @@ 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.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.JBMenuItem
|
||||
import com.intellij.openapi.ui.JBPopupMenu
|
||||
import com.intellij.openapi.vfs.readText
|
||||
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.util.EditorUtil
|
||||
import ee.carlrobert.codegpt.util.StringUtil
|
||||
import okhttp3.sse.EventSource
|
||||
import javax.swing.JPanel
|
||||
|
||||
class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
|
||||
|
||||
private val loadingLabel: JBLabel by lazy {
|
||||
JBLabel(
|
||||
CodeGPTBundle.get("toolwindow.chat.editor.diff.thinking"),
|
||||
AnimatedIcon.Default(),
|
||||
JBLabel.LEFT
|
||||
)
|
||||
companion object {
|
||||
private val logger = thisLogger()
|
||||
}
|
||||
|
||||
private var currentEventSource: EventSource? = null
|
||||
private val loadingPanel = LoadingPanel(
|
||||
CodeGPTBundle.get("toolwindow.chat.editor.diff.thinking")
|
||||
) {
|
||||
handleDone()
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -35,21 +38,28 @@ class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
|
|||
|
||||
override fun initializeRightPanel(rightPanel: JPanel) {
|
||||
if (config.loading) {
|
||||
rightPanel.add(loadingLabel)
|
||||
rightPanel.add(loadingPanel)
|
||||
} else {
|
||||
rightPanel.add(createHeaderActions().component)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(label: String = "Loading...") {
|
||||
fun setLoading(
|
||||
eventSource: EventSource? = null,
|
||||
label: String = CodeGPTBundle.get("toolwindow.chat.editor.diff.applying")
|
||||
) {
|
||||
currentEventSource = eventSource
|
||||
loadingPanel.setText(label)
|
||||
loadingPanel.setEventSource(eventSource)
|
||||
|
||||
runInEdt {
|
||||
loadingLabel.text = label
|
||||
setRightPanelComponent(loadingLabel)
|
||||
setRightPanelComponent(loadingPanel)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDone() {
|
||||
runInEdt {
|
||||
currentEventSource = null
|
||||
setRightPanelComponent(createHeaderActions().component)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,23 +82,27 @@ class DefaultHeaderPanel(config: HeaderConfig) : HeaderPanel(config) {
|
|||
}
|
||||
|
||||
private fun handleApply(project: Project, editor: EditorEx) {
|
||||
val file = virtualFile
|
||||
?: EditorUtil.getSelectedEditor(project)?.virtualFile
|
||||
?: throw IllegalStateException("Virtual file is null")
|
||||
|
||||
val directApplyThreshold = 0.85
|
||||
val coefficient = StringUtil.getDiceCoefficient(editor.document.text, file.readText())
|
||||
if (coefficient > directApplyThreshold) {
|
||||
try {
|
||||
val file = virtualFile
|
||||
?: EditorUtil.getSelectedEditor(project)?.virtualFile
|
||||
?: throw IllegalStateException("Could not find file")
|
||||
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
|
||||
?: throw IllegalStateException("Could not find corresponding ResponseEditorPanel")
|
||||
responseEditorPanel.createDiffEditorForDirectApply(file.readText(), editor.document.text, file)
|
||||
return
|
||||
}
|
||||
?: throw IllegalStateException("Could not find editor panel")
|
||||
|
||||
val responseEditorPanel = editor.component.parent as? ResponseEditorPanel
|
||||
?: throw IllegalStateException("Could not find corresponding ResponseEditorPanel")
|
||||
responseEditorPanel.applyCodeAsync(editor.document.text, file, editor)
|
||||
setLoading("Editing...")
|
||||
val directApplyThreshold = 0.85
|
||||
val coefficient = StringUtil.getDiceCoefficient(editor.document.text, file.readText())
|
||||
if (coefficient > directApplyThreshold) {
|
||||
responseEditorPanel.createDiffEditorForDirectApply(
|
||||
file.readText(),
|
||||
editor.document.text,
|
||||
file
|
||||
)
|
||||
return
|
||||
}
|
||||
responseEditorPanel.applyCodeAsync(editor.document.text, file, editor, this)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createToolbar(actionGroup: ActionGroup): ActionToolbar {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,67 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat.editor.header
|
||||
|
||||
import com.intellij.diff.tools.fragmented.UnifiedDiffChange
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.icons.AllIcons.Actions.Close
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffAcceptedPanel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import okhttp3.sse.EventSource
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.*
|
||||
|
||||
interface DiffHeaderActions {
|
||||
fun onAcceptAll()
|
||||
fun onOpenDiff()
|
||||
fun onClose()
|
||||
}
|
||||
|
||||
class DiffHeaderPanel(
|
||||
config: HeaderConfig,
|
||||
retry: Boolean,
|
||||
private val actions: DiffHeaderActions
|
||||
private val actions: DiffHeaderActions,
|
||||
private var eventSource: EventSource? = null
|
||||
) : HeaderPanel(config) {
|
||||
|
||||
private val loadingLabel: JBLabel = JBLabel(
|
||||
if (retry) CodeGPTBundle.get("toolwindow.chat.editor.diff.retrying")
|
||||
else CodeGPTBundle.get("toolwindow.chat.editor.diff.reading"),
|
||||
AnimatedIcon.Default(),
|
||||
JBLabel.LEFT
|
||||
)
|
||||
private val loadingPanel = LoadingPanel(
|
||||
when {
|
||||
retry -> CodeGPTBundle.get("toolwindow.chat.editor.diff.retrying")
|
||||
eventSource != null -> CodeGPTBundle.get("toolwindow.chat.editor.diff.applying")
|
||||
else -> CodeGPTBundle.get("toolwindow.chat.editor.diff.editing")
|
||||
},
|
||||
eventSource
|
||||
) {
|
||||
handleDone()
|
||||
}
|
||||
|
||||
private val actionLinksPanel = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
isVisible = false
|
||||
add(ActionLink("View Diff") { actions.onOpenDiff() })
|
||||
add(separator())
|
||||
add(Box.createHorizontalStrut(6))
|
||||
add(ActionLink(CodeGPTBundle.get("shared.acceptAll")) { actions.onAcceptAll() })
|
||||
add(separator())
|
||||
add(IconActionButton(
|
||||
object : AnAction("Close", "Close the diff view", Close) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
actions.onClose()
|
||||
}
|
||||
},
|
||||
"close-diff"
|
||||
))
|
||||
}
|
||||
|
||||
init {
|
||||
setupUI()
|
||||
runInEdt {
|
||||
loadingPanel.isVisible = true
|
||||
loadingPanel.setEventSource(eventSource)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initializeRightPanel(rightPanel: JPanel) {
|
||||
|
|
@ -48,22 +69,24 @@ class DiffHeaderPanel(
|
|||
|
||||
rightPanel.apply {
|
||||
add(actionLinksPanel)
|
||||
add(loadingLabel)
|
||||
add(loadingPanel)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDone() {
|
||||
eventSource = null
|
||||
runInEdt {
|
||||
actionLinksPanel.isVisible = true
|
||||
loadingLabel.isVisible = false
|
||||
loadingPanel.isVisible = false
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleChangesApplied(before: String, after: String, patches: List<UnifiedDiffChange>) {
|
||||
eventSource = null
|
||||
actionLinksPanel.isVisible = false
|
||||
loadingLabel.isVisible = false
|
||||
loadingPanel.isVisible = false
|
||||
|
||||
virtualFile?.let {
|
||||
val diffAcceptedPanel = DiffAcceptedPanel(config.project, it, before, after, patches)
|
||||
|
|
@ -81,8 +104,9 @@ class DiffHeaderPanel(
|
|||
|
||||
fun editing() {
|
||||
runInEdt {
|
||||
loadingLabel.text = CodeGPTBundle.get("toolwindow.chat.editor.diff.editing")
|
||||
loadingLabel.isVisible = true
|
||||
loadingPanel.setText(CodeGPTBundle.get("toolwindow.chat.editor.diff.editing"))
|
||||
loadingPanel.isVisible = true
|
||||
loadingPanel.showStopButton(eventSource != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
|
|||
|
||||
private val statsComponent = SimpleColoredComponent().apply {
|
||||
font = JBUI.Fonts.smallFont()
|
||||
preferredSize = Dimension(preferredSize.width, 16)
|
||||
}
|
||||
private val errorLabel = JBLabel(AllIcons.General.Error).apply {
|
||||
isVisible = config.error != null
|
||||
|
|
@ -53,7 +54,6 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
|
|||
}
|
||||
private val rightPanel = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
alignmentY = 0.5f
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
|
|
@ -72,14 +72,8 @@ abstract class HeaderPanel(protected val config: HeaderConfig) : BorderLayoutPan
|
|||
protected fun setupUI() {
|
||||
setupPanelAppearance()
|
||||
addToLeft(createLeftPanel(virtualFile))
|
||||
rightPanel.removeAll()
|
||||
initializeRightPanel(rightPanel)
|
||||
|
||||
val rightCenteringPanel = JPanel(BorderLayout()).apply {
|
||||
isOpaque = false
|
||||
add(rightPanel, BorderLayout.CENTER)
|
||||
}
|
||||
addToRight(rightCenteringPanel)
|
||||
addToRight(rightPanel)
|
||||
}
|
||||
|
||||
protected fun setRightPanelComponent(component: JComponent?) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.chat.editor.header
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import okhttp3.sse.EventSource
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JPanel
|
||||
|
||||
class LoadingPanel(
|
||||
initialText: String,
|
||||
private var eventSource: EventSource? = null,
|
||||
private val onCancel: (() -> Unit)? = null
|
||||
) : JPanel() {
|
||||
|
||||
private val loadingLabel = JBLabel(initialText, AnimatedIcon.Default(), JBLabel.LEFT)
|
||||
|
||||
private val stopButton = IconActionButton(
|
||||
object : AnAction("Stop", "Stop the current operation", AllIcons.Actions.Suspend) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
eventSource?.cancel()
|
||||
onCancel?.invoke()
|
||||
}
|
||||
},
|
||||
"stop-operation"
|
||||
).apply {
|
||||
isVisible = eventSource != null
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
add(loadingLabel)
|
||||
add(SeparatorComponent(
|
||||
ColorUtil.fromHex("#48494b"),
|
||||
SeparatorOrientation.VERTICAL
|
||||
).apply {
|
||||
setVGap(4)
|
||||
setHGap(6)
|
||||
})
|
||||
add(stopButton)
|
||||
}
|
||||
|
||||
fun setText(text: String) {
|
||||
loadingLabel.text = text
|
||||
}
|
||||
|
||||
fun setEventSource(source: EventSource?) {
|
||||
eventSource = source
|
||||
stopButton.isVisible = source != null
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun showStopButton(show: Boolean) {
|
||||
stopButton.isVisible = show && eventSource != null
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
|
|||
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.HeaderConfig
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.parser.Segment
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil
|
||||
import okhttp3.sse.EventSource
|
||||
import java.util.*
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JButton
|
||||
|
|
@ -39,7 +40,8 @@ abstract class DiffEditorState(
|
|||
override val segment: Segment,
|
||||
override val project: Project,
|
||||
val diffViewer: UnifiedDiffViewer?,
|
||||
val virtualFile: VirtualFile?
|
||||
val virtualFile: VirtualFile?,
|
||||
private val eventSource: EventSource? = null
|
||||
) : EditorState {
|
||||
|
||||
companion object {
|
||||
|
|
@ -91,6 +93,10 @@ abstract class DiffEditorState(
|
|||
override fun onOpenDiff() {
|
||||
openDiff()
|
||||
}
|
||||
|
||||
override fun onClose() {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
return DiffHeaderPanel(
|
||||
|
|
@ -102,12 +108,15 @@ abstract class DiffEditorState(
|
|||
false
|
||||
),
|
||||
readOnly,
|
||||
actions
|
||||
actions,
|
||||
eventSource
|
||||
)
|
||||
}
|
||||
|
||||
abstract fun applyAllChanges()
|
||||
|
||||
abstract fun handleClose()
|
||||
|
||||
private fun openDiff() {
|
||||
if (virtualFile == null) {
|
||||
throw IllegalStateException("Virtual file is null")
|
||||
|
|
|
|||
|
|
@ -12,21 +12,24 @@ 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
|
||||
import okhttp3.sse.EventSource
|
||||
|
||||
class EditorStateManager(private val project: Project) {
|
||||
|
||||
private var currentState: EditorState? = null
|
||||
private var diffEditorManager: DiffEditorManager? = null
|
||||
|
||||
fun createFromSegment(segment: Segment, readOnly: Boolean = false): EditorState {
|
||||
fun createFromSegment(segment: Segment, readOnly: Boolean = false, eventSource: EventSource? = null, originalSuggestion: String? = null): EditorState {
|
||||
val editor = EditorFactory.createEditor(project, segment)
|
||||
val state = if (editor.editorKind == EditorKind.DIFF) {
|
||||
createDiffState(editor, segment)
|
||||
createDiffState(editor, segment, eventSource, originalSuggestion)
|
||||
} else {
|
||||
RegularEditorState(editor, segment, project)
|
||||
}
|
||||
|
||||
runInEdt {
|
||||
EditorFactory.configureEditor(editor, state.createHeaderComponent(readOnly))
|
||||
val headerComponent = state.createHeaderComponent(readOnly)
|
||||
EditorFactory.configureEditor(editor, headerComponent)
|
||||
}
|
||||
|
||||
RESPONSE_EDITOR_STATE_KEY.set(editor, state)
|
||||
|
|
@ -55,7 +58,8 @@ class EditorStateManager(private val project: Project) {
|
|||
FailedDiffEditorState(newEditor, newSegment, project, searchContent, replaceContent)
|
||||
|
||||
runInEdt {
|
||||
EditorFactory.configureEditor(newEditor, newState.createHeaderComponent(false))
|
||||
val headerComponent = newState.createHeaderComponent(false)
|
||||
EditorFactory.configureEditor(newEditor, headerComponent)
|
||||
}
|
||||
|
||||
this.currentState = newState
|
||||
|
|
@ -67,19 +71,27 @@ class EditorStateManager(private val project: Project) {
|
|||
return currentState
|
||||
}
|
||||
|
||||
private fun createDiffState(editor: EditorEx, segment: Segment): EditorState {
|
||||
fun clearCurrentState() {
|
||||
currentState = null
|
||||
}
|
||||
|
||||
private fun createDiffState(editor: EditorEx, segment: Segment, eventSource: EventSource? = null, originalSuggestion: String? = null): EditorState {
|
||||
val virtualFile = getVirtualFile(segment.filePath)
|
||||
val diffViewer = ResponseEditorPanel.RESPONSE_EDITOR_DIFF_VIEWER_KEY.get(editor)
|
||||
val diffEditorManager = DiffEditorManager(project, diffViewer, virtualFile)
|
||||
this.diffEditorManager = diffEditorManager
|
||||
return StandardDiffEditorState(
|
||||
|
||||
val state = StandardDiffEditorState(
|
||||
editor,
|
||||
segment,
|
||||
project,
|
||||
diffViewer,
|
||||
virtualFile,
|
||||
diffEditorManager
|
||||
diffEditorManager,
|
||||
eventSource,
|
||||
originalSuggestion
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
private fun getVirtualFile(filePath: String?): VirtualFile? {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import com.intellij.openapi.editor.EditorKind
|
|||
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 com.intellij.util.application
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.diff.DiffEditorManager
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.editor.header.DiffHeaderPanel
|
||||
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 okhttp3.sse.EventSource
|
||||
|
||||
class StandardDiffEditorState(
|
||||
editor: EditorEx,
|
||||
|
|
@ -22,9 +24,11 @@ class StandardDiffEditorState(
|
|||
project: Project,
|
||||
diffViewer: UnifiedDiffViewer?,
|
||||
virtualFile: VirtualFile?,
|
||||
private val diffEditorManager: DiffEditorManager
|
||||
) : DiffEditorState(editor, segment, project, diffViewer, virtualFile) {
|
||||
|
||||
private val diffEditorManager: DiffEditorManager,
|
||||
eventSource: EventSource? = null,
|
||||
private val originalSuggestion: String? = null
|
||||
) : DiffEditorState(editor, segment, project, diffViewer, virtualFile, eventSource) {
|
||||
|
||||
override fun applyAllChanges() {
|
||||
val before = diffViewer?.getDocument(Side.LEFT)?.text ?: return
|
||||
val after = diffViewer.getDocument(Side.RIGHT).text
|
||||
|
|
@ -45,7 +49,7 @@ class StandardDiffEditorState(
|
|||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
diffEditorManager.updateDiffContent(search, replace)
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)
|
||||
?.updateDiffStats(diffViewer?.diffChanges ?: emptyList())
|
||||
|
|
@ -59,4 +63,18 @@ class StandardDiffEditorState(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleClose() {
|
||||
runInEdt {
|
||||
val responsePanel = editor.component.parent as? ResponseEditorPanel ?: return@runInEdt
|
||||
val contentToKeep = originalSuggestion ?: when (segment) {
|
||||
is SearchReplace -> segment.replace
|
||||
is ReplaceWaiting -> segment.replace
|
||||
else -> diffViewer?.getDocument(Side.RIGHT)?.text ?: ""
|
||||
}
|
||||
responsePanel.replaceEditorWithSegment(
|
||||
Code(contentToKeep, segment.language, segment.filePath)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,10 +190,10 @@ toolwindow.chat.editor.action.autoApply.description=Apply suggested changes auto
|
|||
toolwindow.chat.editor.action.autoApply.noActiveFile=Active file not found
|
||||
toolwindow.chat.editor.action.autoApply.fileTooLarge=Active file too large to process
|
||||
toolwindow.chat.editor.action.autoApply.reject=Reject All
|
||||
toolwindow.chat.editor.diff.reading=Reading...
|
||||
toolwindow.chat.editor.diff.thinking=Thinking...
|
||||
toolwindow.chat.editor.diff.editing=Editing...
|
||||
toolwindow.chat.editor.diff.retrying=Retrying...
|
||||
toolwindow.chat.editor.diff.applying=Applying
|
||||
toolwindow.chat.editor.diff.thinking=Thinking
|
||||
toolwindow.chat.editor.diff.editing=Editing
|
||||
toolwindow.chat.editor.diff.retrying=Retrying
|
||||
toolwindow.chat.editor.action.autoApply.error=Something went wrong while applying changes. {0}
|
||||
toolwindow.chat.editor.action.autoApply.taskTitle=Apply changes
|
||||
toolwindow.chat.editor.action.autoApply.loadingMessage=ProxyAI: Applying changes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue