mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-04-28 03:30:48 +00:00
feat: inline edit quick question
This commit is contained in:
parent
3a5ca4edf8
commit
e7e9a2d9d8
28 changed files with 1029 additions and 391 deletions
|
|
@ -113,7 +113,6 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
tagManager,
|
||||
this::handleSubmit,
|
||||
this::handleCancel,
|
||||
true,
|
||||
true);
|
||||
userInputPanel.requestFocus();
|
||||
rootPanel = createRootPanel();
|
||||
|
|
@ -334,6 +333,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
panel.addContent(new ChatMessageResponseBody(
|
||||
project,
|
||||
false,
|
||||
false,
|
||||
message.isWebSearchIncluded(),
|
||||
fileContextIncluded || message.getDocumentationDetails() != null,
|
||||
true,
|
||||
|
|
@ -523,7 +523,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
private ResponseMessagePanel getResponseMessagePanel(Message message) {
|
||||
var response = message.getResponse() == null ? "" : message.getResponse();
|
||||
var messageResponseBody =
|
||||
new ChatMessageResponseBody(project, this).withResponse(response);
|
||||
new ChatMessageResponseBody(project, false, this).withResponse(response);
|
||||
|
||||
var responseMessagePanel = new ResponseMessagePanel();
|
||||
responseMessagePanel.addContent(messageResponseBody);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ public class ChatMessageResponseBody extends JPanel {
|
|||
private final Disposable parentDisposable;
|
||||
private final SseMessageParser streamOutputParser;
|
||||
private final boolean readOnly;
|
||||
private final boolean compact;
|
||||
private final DefaultListModel<WebSearchEventDetails> webpageListModel = new DefaultListModel<>();
|
||||
private final WebpageList webpageList = new WebpageList(webpageListModel);
|
||||
private final ResponseBodyProgressPanel progressPanel = new ResponseBodyProgressPanel();
|
||||
|
|
@ -104,13 +105,14 @@ public class ChatMessageResponseBody extends JPanel {
|
|||
.withBorder(JBUI.Borders.empty(4, 0));
|
||||
}
|
||||
|
||||
public ChatMessageResponseBody(Project project, Disposable parentDisposable) {
|
||||
this(project, false, false, false, false, parentDisposable);
|
||||
public ChatMessageResponseBody(Project project, boolean compact, Disposable parentDisposable) {
|
||||
this(project, false, compact, false, false, false, parentDisposable);
|
||||
}
|
||||
|
||||
public ChatMessageResponseBody(
|
||||
Project project,
|
||||
boolean readOnly,
|
||||
boolean compact,
|
||||
boolean webSearchIncluded,
|
||||
boolean withProgress,
|
||||
boolean withLoading,
|
||||
|
|
@ -119,6 +121,7 @@ public class ChatMessageResponseBody extends JPanel {
|
|||
this.parentDisposable = parentDisposable;
|
||||
this.streamOutputParser = new SseMessageParser();
|
||||
this.readOnly = readOnly;
|
||||
this.compact = compact;
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
setOpaque(false);
|
||||
|
|
@ -380,7 +383,7 @@ public class ChatMessageResponseBody extends JPanel {
|
|||
hideCaret();
|
||||
currentlyProcessedTextPane = null;
|
||||
currentlyProcessedEditorPanel =
|
||||
new ResponseEditorPanel(project, item, readOnly, parentDisposable);
|
||||
new ResponseEditorPanel(project, item, readOnly, compact, parentDisposable);
|
||||
contentPanel.add(currentlyProcessedEditorPanel);
|
||||
contentPanel.revalidate();
|
||||
contentPanel.repaint();
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ package ee.carlrobert.codegpt.completions
|
|||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import ee.carlrobert.codegpt.EncodingManager
|
||||
import ee.carlrobert.codegpt.nextedit.NextEditPromptUtil
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.CURSOR_MARKER
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.DEFAULT_LINES_AFTER
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.DEFAULT_LINES_BEFORE
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.MAX_EDITABLE_REGION_LINES
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.MAX_RECENTLY_VIEWED_SNIPPETS
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestFactory.Companion.RECENTLY_VIEWED_LINES
|
||||
import ee.carlrobert.codegpt.completions.factory.*
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.nextedit.NextEditPromptUtil
|
||||
import ee.carlrobert.codegpt.psistructure.ClassStructureSerializer
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState
|
||||
import ee.carlrobert.codegpt.settings.prompts.FilteredPromptsService
|
||||
import ee.carlrobert.codegpt.settings.prompts.PersonaDetails
|
||||
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
|
|
@ -26,6 +26,7 @@ import ee.carlrobert.llm.completion.CompletionRequest
|
|||
interface CompletionRequestFactory {
|
||||
fun createChatRequest(params: ChatCompletionParameters): CompletionRequest
|
||||
fun createInlineEditRequest(params: InlineEditCompletionParameters): CompletionRequest
|
||||
fun createInlineEditQuestionRequest(parameters: ChatCompletionParameters): CompletionRequest
|
||||
fun createAutoApplyRequest(params: AutoApplyParameters): CompletionRequest
|
||||
fun createCommitMessageRequest(params: CommitMessageCompletionParameters): CompletionRequest
|
||||
fun createLookupRequest(params: LookupCompletionParameters): CompletionRequest
|
||||
|
|
@ -71,6 +72,45 @@ abstract class BaseRequestFactory : CompletionRequestFactory {
|
|||
private const val DEFAULT_MAX_TOKENS = 4096
|
||||
}
|
||||
|
||||
override fun createInlineEditQuestionRequest(parameters: ChatCompletionParameters): CompletionRequest {
|
||||
val systemPrompt = """
|
||||
You are an Inline Edit assistant for a single open file.
|
||||
Respond in two parts:
|
||||
|
||||
1) Explanation (concise):
|
||||
- 3–5 short bullets max.
|
||||
- Summarize what will change and why.
|
||||
- Reference functions/classes by name. Do not paste full files.
|
||||
|
||||
2) Update Snippet(s):
|
||||
- Provide ONLY partial changes as one or more fenced code blocks using triple backticks with the correct language (```python, ```kotlin, etc.).
|
||||
- Do NOT include any special tags.
|
||||
- Use minimal necessary context; indicate gaps with language-appropriate comments like "// ... existing code ..." or "# ... existing code ...".
|
||||
- Include only changed/new lines with at most 1–3 lines of surrounding context when needed.
|
||||
- Prefer stable anchors (function/class signatures, imports) to locate insertion points.
|
||||
- Never output entire files or unrelated edits.
|
||||
""".trimIndent()
|
||||
|
||||
val userPrompt = getPromptWithFilesContext(parameters)
|
||||
|
||||
val newParams = ChatCompletionParameters
|
||||
.builder(parameters.conversation, Message(userPrompt))
|
||||
.sessionId(parameters.sessionId)
|
||||
.conversationType(parameters.conversationType)
|
||||
.retry(parameters.retry)
|
||||
.imageDetails(parameters.imageDetails)
|
||||
.history(parameters.history)
|
||||
.referencedFiles(parameters.referencedFiles)
|
||||
.personaDetails(PersonaDetails(-1L, "Inline Edit Guidance", systemPrompt))
|
||||
.psiStructure(parameters.psiStructure)
|
||||
.project(parameters.project)
|
||||
.chatMode(ChatMode.ASK)
|
||||
.featureType(FeatureType.INLINE_EDIT)
|
||||
.build()
|
||||
|
||||
return createChatRequest(newParams)
|
||||
}
|
||||
|
||||
data class InlineEditPrompts(val systemPrompt: String, val userPrompt: String)
|
||||
|
||||
protected fun prepareInlineEditPrompts(params: InlineEditCompletionParameters): InlineEditPrompts {
|
||||
|
|
@ -259,8 +299,13 @@ abstract class BaseRequestFactory : CompletionRequestFactory {
|
|||
val (project, fileName, filePath, fileContent, caretOffset, gitDiff, _) = params
|
||||
|
||||
val encodingManager = EncodingManager.getInstance()
|
||||
val prefixContent = encodingManager.truncateText(fileContent.substring(0, caretOffset), 4096, true)
|
||||
val suffixContent = encodingManager.truncateText(fileContent.substring(caretOffset, fileContent.length), 4096, false)
|
||||
val prefixContent =
|
||||
encodingManager.truncateText(fileContent.substring(0, caretOffset), 4096, true)
|
||||
val suffixContent = encodingManager.truncateText(
|
||||
fileContent.substring(caretOffset, fileContent.length),
|
||||
4096,
|
||||
false
|
||||
)
|
||||
|
||||
val truncatedContent = prefixContent + suffixContent
|
||||
val adjustedCaretOffset = prefixContent.length
|
||||
|
|
@ -324,4 +369,4 @@ abstract class BaseRequestFactory : CompletionRequestFactory {
|
|||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import java.nio.file.Path
|
|||
class OpenAIRequestFactory : BaseRequestFactory() {
|
||||
|
||||
override fun createChatRequest(params: ChatCompletionParameters): OpenAIChatCompletionRequest {
|
||||
val model = ModelSelectionService.getInstance().getModelForFeature(FeatureType.CHAT)
|
||||
val model = ModelSelectionService.getInstance().getModelForFeature(params.featureType)
|
||||
val configuration = service<ConfigurationSettings>().state
|
||||
val requestBuilder: OpenAIChatCompletionRequest.Builder =
|
||||
OpenAIChatCompletionRequest.Builder(buildOpenAIMessages(model, params))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package ee.carlrobert.codegpt.inlineedit
|
||||
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ee.carlrobert.codegpt.codecompletions.CompletionProgressNotifier
|
||||
import ee.carlrobert.codegpt.completions.ChatCompletionParameters
|
||||
import ee.carlrobert.codegpt.completions.CompletionResponseEventListener
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import ee.carlrobert.llm.client.openai.completion.ErrorDetails
|
||||
|
||||
class InlineAskResponseListener(
|
||||
private val project: Project,
|
||||
private val inlay: InlineEditInlay,
|
||||
) : CompletionResponseEventListener {
|
||||
|
||||
private val logger = Logger.getInstance(InlineAskResponseListener::class.java)
|
||||
private val builder = StringBuilder()
|
||||
|
||||
override fun handleRequestOpen() {
|
||||
runInEdt { inlay.setThinkingVisible(true) }
|
||||
CompletionProgressNotifier.update(project, true)
|
||||
}
|
||||
|
||||
override fun handleMessage(message: String) {
|
||||
builder.append(message)
|
||||
inlay.updateAskResponseStream(message)
|
||||
inlay.setAskLastAssistantResponse(builder.toString())
|
||||
}
|
||||
|
||||
override fun handleCompleted(fullMessage: String, callParameters: ChatCompletionParameters) {
|
||||
try {
|
||||
runInEdt {
|
||||
inlay.onCompletionFinished()
|
||||
inlay.setAskLastAssistantResponse(fullMessage)
|
||||
inlay.updateApplyVisibilityAfterComplete(fullMessage)
|
||||
CompletionProgressNotifier.update(project, false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Inline Ask completion finalize failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleError(error: ErrorDetails?, ex: Throwable?) {
|
||||
runInEdt {
|
||||
inlay.onCompletionFinished()
|
||||
CompletionProgressNotifier.update(project, false)
|
||||
val message = error?.message ?: ex?.message ?: "Something went wrong"
|
||||
OverlayUtil.showNotification(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
package ee.carlrobert.codegpt.inlineedit
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.invokeLater
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
|
|
@ -13,40 +16,49 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent
|
|||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.actions.editor.EditorComponentInlaysManager
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.inlineedit.engine.ApplyContext
|
||||
import ee.carlrobert.codegpt.inlineedit.engine.InlineEditEngineImpl
|
||||
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import ee.carlrobert.codegpt.ui.components.BadgeChip
|
||||
import ee.carlrobert.codegpt.ui.components.InlineEditChips
|
||||
import ee.carlrobert.codegpt.ui.textarea.ConversationTagProcessor
|
||||
import ee.carlrobert.codegpt.ui.textarea.TagProcessorFactory
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
import ee.carlrobert.codegpt.util.GitUtil
|
||||
import ee.carlrobert.codegpt.util.MarkdownUtil
|
||||
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.AbstractAction
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.Timer
|
||||
import javax.swing.*
|
||||
|
||||
data class ObservableProperties(
|
||||
val submitted: AtomicBooleanProperty = AtomicBooleanProperty(false),
|
||||
val accepted: AtomicBooleanProperty = AtomicBooleanProperty(false),
|
||||
val loading: AtomicBooleanProperty = AtomicBooleanProperty(false),
|
||||
val hasPendingChanges: AtomicBooleanProperty = AtomicBooleanProperty(false),
|
||||
)
|
||||
|
||||
class InlineEditInlay(private var editor: Editor) : Disposable {
|
||||
|
|
@ -111,7 +123,6 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
submissionHandler.restorePreviousPrompt()
|
||||
}
|
||||
},
|
||||
showModeSelector = false,
|
||||
withRemovableSelectedEditorTag = false
|
||||
).apply {
|
||||
isOpaque = true
|
||||
|
|
@ -125,11 +136,55 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
private var askResponseBody: ChatMessageResponseBody? = null
|
||||
private var askContainer: BorderLayoutPanel? = null
|
||||
private var askPopup: JBPopup? = null
|
||||
private var askApplyChip: BadgeChip? = null
|
||||
private var lastAssistantResponse: String = ""
|
||||
private val askMinHeight: Int = JBUI.scale(200)
|
||||
private val askMaxHeight: Int = JBUI.scale(520)
|
||||
|
||||
private data class AskPanelContext(
|
||||
val editorEx: EditorEx,
|
||||
val editorComp: JComponent,
|
||||
val panelWidth: Int,
|
||||
val panelLeft: Int,
|
||||
val panelTop: Int,
|
||||
val panelBottom: Int,
|
||||
val spaceAbove: Int,
|
||||
val spaceBelow: Int,
|
||||
val placeAbove: Boolean,
|
||||
)
|
||||
|
||||
private fun getAskPanelContext(): AskPanelContext? {
|
||||
val editorEx = editor as? EditorEx ?: return null
|
||||
val editorComp = editorEx.contentComponent
|
||||
val panelSize = if (mainContainer.width > 0) mainContainer.size else mainContainer.preferredSize
|
||||
val panelPoint = SwingUtilities.convertPoint(mainContainer, 0, 0, editorComp)
|
||||
val panelTop = panelPoint.y
|
||||
val panelBottom = panelTop + panelSize.height
|
||||
val visible = editorEx.scrollingModel.visibleArea
|
||||
val spaceAbove = panelTop - visible.y - JBUI.scale(6)
|
||||
|
||||
return AskPanelContext(
|
||||
editorEx = editorEx,
|
||||
editorComp = editorComp,
|
||||
panelWidth = panelSize.width,
|
||||
panelLeft = panelPoint.x,
|
||||
panelTop = panelTop,
|
||||
panelBottom = panelBottom,
|
||||
spaceAbove = spaceAbove,
|
||||
spaceBelow = (visible.y + visible.height) - panelBottom - JBUI.scale(6),
|
||||
placeAbove = spaceAbove >= askMinHeight,
|
||||
)
|
||||
}
|
||||
|
||||
private val mainContainer = BorderLayoutPanel().apply {
|
||||
isOpaque = true
|
||||
add(userInputPanel, BorderLayout.CENTER)
|
||||
// Place input at SOUTH so it keeps its preferred height and doesn't stretch
|
||||
add(userInputPanel, BorderLayout.SOUTH)
|
||||
|
||||
border = JBUI.Borders.empty(8, 12, 8, 12)
|
||||
border = JBUI.Borders.empty(4, 8, 2, 8)
|
||||
background = userInputPanel.background ?: JBColor.background()
|
||||
|
||||
isFocusable = true
|
||||
|
|
@ -190,9 +245,201 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
val editorEx = editor as EditorEx
|
||||
val sessionConversation = Conversation().apply {
|
||||
projectPath = editorEx.project?.basePath
|
||||
title = "Inline Edit (${editorEx.virtualFile?.name ?: "untitled"})"
|
||||
val fileName = editorEx.virtualFile?.name ?: CodeGPTBundle.get("inlineEdit.conversation.untitled")
|
||||
title = CodeGPTBundle.get("inlineEdit.conversation.title", fileName)
|
||||
}
|
||||
submissionHandler = InlineEditSubmissionHandler(editor, observableProperties, sessionConversation)
|
||||
submissionHandler =
|
||||
InlineEditSubmissionHandler(editor, observableProperties, sessionConversation)
|
||||
}
|
||||
|
||||
fun isQuickQuestionEnabled() = userInputPanel.isQuickQuestionEnabled()
|
||||
|
||||
private fun buildAskContainer(): JComponent {
|
||||
val container = BorderLayoutPanel().apply {
|
||||
isOpaque = true
|
||||
background = userInputPanel.background ?: JBColor.background()
|
||||
border = JBUI.Borders.empty(6, 8)
|
||||
minimumSize = Dimension(JBUI.scale(600), JBUI.scale(400))
|
||||
}
|
||||
|
||||
val responseBody = ChatMessageResponseBody(
|
||||
project,
|
||||
true,
|
||||
this
|
||||
).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
askResponseBody = responseBody
|
||||
|
||||
val scrollPane = JBScrollPane(responseBody).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
isOpaque = false
|
||||
viewport.isOpaque = false
|
||||
}
|
||||
|
||||
val copyButton = IconActionButton(
|
||||
object :
|
||||
AnAction(
|
||||
CodeGPTBundle.get("shared.copy"),
|
||||
CodeGPTBundle.get("shared.copyToClipboard"),
|
||||
com.intellij.icons.AllIcons.Actions.Copy
|
||||
) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val text = lastAssistantResponse
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
clipboard.setContents(StringSelection(text), null)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabled = lastAssistantResponse.isNotBlank()
|
||||
}
|
||||
},
|
||||
"COPY_MD"
|
||||
)
|
||||
|
||||
val applyChip = BadgeChip(CodeGPTBundle.get("shared.apply"), InlineEditChips.GREEN, { handleApply() }).apply {
|
||||
isVisible = false
|
||||
isEnabled = false
|
||||
}
|
||||
askApplyChip = applyChip
|
||||
|
||||
val header = JPanel(FlowLayout(FlowLayout.RIGHT, JBUI.scale(4), JBUI.scale(4))).apply {
|
||||
isOpaque = false
|
||||
add(copyButton)
|
||||
add(applyChip)
|
||||
}
|
||||
|
||||
container.add(header, BorderLayout.NORTH)
|
||||
container.add(scrollPane, BorderLayout.CENTER)
|
||||
|
||||
askContainer = container
|
||||
return container
|
||||
}
|
||||
|
||||
private fun showAskPopup() {
|
||||
runInEdt {
|
||||
if (askPopup?.isVisible == true) return@runInEdt
|
||||
val content = askContainer ?: buildAskContainer()
|
||||
val ctx = getAskPanelContext() ?: return@runInEdt
|
||||
val availableHeight = if (ctx.placeAbove) ctx.spaceAbove else ctx.spaceBelow
|
||||
val pref = content.preferredSize
|
||||
val targetH =
|
||||
pref.height.coerceIn(askMinHeight, askMaxHeight).coerceAtMost(availableHeight)
|
||||
.coerceAtLeast(askMinHeight)
|
||||
|
||||
content.preferredSize = Dimension(ctx.panelWidth, targetH)
|
||||
|
||||
val builder = JBPopupFactory.getInstance()
|
||||
.createComponentPopupBuilder(content, null)
|
||||
.setRequestFocus(false)
|
||||
.setFocusable(false)
|
||||
.setResizable(true)
|
||||
.setMovable(true)
|
||||
.setCancelOnClickOutside(false)
|
||||
.setCancelOnOtherWindowOpen(false)
|
||||
.setCancelOnWindowDeactivation(false)
|
||||
.setMinSize(Dimension(ctx.panelWidth, askMinHeight))
|
||||
|
||||
val popup = builder.createPopup()
|
||||
askPopup = popup
|
||||
|
||||
popup.size = Dimension(ctx.panelWidth, targetH)
|
||||
|
||||
val margin = JBUI.scale(6)
|
||||
val anchorPoint = if (ctx.placeAbove) {
|
||||
Point(ctx.panelLeft, ctx.panelTop + targetH - margin)
|
||||
} else {
|
||||
Point(ctx.panelLeft, ctx.panelBottom + margin)
|
||||
}
|
||||
popup.show(RelativePoint(ctx.editorComp, anchorPoint))
|
||||
adjustAskPopupSize()
|
||||
}
|
||||
}
|
||||
|
||||
fun hideAskPopup() {
|
||||
runInEdt {
|
||||
askPopup?.cancel()
|
||||
askPopup = null
|
||||
resetApplyChip()
|
||||
askResponseBody?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAskContainer() {
|
||||
runInEdt {
|
||||
showAskPopup()
|
||||
askResponseBody?.clear()
|
||||
resetApplyChip()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAskResponseStream(partial: String) {
|
||||
runInEdt {
|
||||
showAskPopup()
|
||||
askResponseBody?.updateMessage(partial)
|
||||
adjustAskPopupSize()
|
||||
}
|
||||
}
|
||||
|
||||
fun setAskLastAssistantResponse(content: String) {
|
||||
lastAssistantResponse = content
|
||||
val hasBlocks = MarkdownUtil.extractCodeBlocks(content).isNotEmpty()
|
||||
setApplyChip(enabled = hasBlocks)
|
||||
}
|
||||
|
||||
fun updateApplyVisibilityAfterComplete(fullMessage: String) {
|
||||
val hasBlocks = MarkdownUtil.extractCodeBlocks(fullMessage).isNotEmpty()
|
||||
setApplyChip(enabled = hasBlocks, visible = hasBlocks)
|
||||
adjustAskPopupSize()
|
||||
}
|
||||
|
||||
private fun adjustAskPopupSize() {
|
||||
val popup = askPopup ?: return
|
||||
val container = askContainer ?: return
|
||||
val ctx = getAskPanelContext() ?: return
|
||||
|
||||
container.revalidate()
|
||||
val pref = container.preferredSize
|
||||
val targetH = pref.height.coerceIn(askMinHeight, askMaxHeight)
|
||||
|
||||
if (popup.size.width != ctx.panelWidth || popup.size.height != targetH) {
|
||||
popup.size = Dimension(ctx.panelWidth, targetH)
|
||||
}
|
||||
|
||||
val margin = JBUI.scale(6)
|
||||
val anchorPoint = if (ctx.placeAbove) {
|
||||
Point(ctx.panelLeft, ctx.panelTop - targetH - margin)
|
||||
} else {
|
||||
Point(ctx.panelLeft, ctx.panelBottom + margin)
|
||||
}
|
||||
popup.setLocation(RelativePoint(ctx.editorComp, anchorPoint).screenPoint)
|
||||
}
|
||||
|
||||
private fun resetApplyChip() {
|
||||
askApplyChip?.isVisible = false
|
||||
askApplyChip?.isEnabled = false
|
||||
}
|
||||
|
||||
private fun setApplyChip(enabled: Boolean, visible: Boolean? = null) {
|
||||
askApplyChip?.isEnabled = enabled
|
||||
if (visible != null) askApplyChip?.isVisible = visible
|
||||
}
|
||||
|
||||
private fun handleApply() {
|
||||
hideAskPopup()
|
||||
|
||||
val editorEx = editor as? EditorEx ?: return
|
||||
val ctx = ApplyContext(
|
||||
editorEx,
|
||||
this,
|
||||
submissionHandler,
|
||||
userInputPanel.text,
|
||||
lastAssistantResponse
|
||||
)
|
||||
InlineEditEngineImpl().apply(ctx)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
|
|
@ -241,24 +488,25 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
runInEdt {
|
||||
try {
|
||||
val session = editor.getUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_SESSION)
|
||||
val hasPending = (session?.hasPendingHunks() == true) || observableProperties.hasPendingChanges.get()
|
||||
val accepted = observableProperties.accepted.get()
|
||||
val renderer = editor.getUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_RENDERER)
|
||||
val hasPending = (session?.hasPendingHunks() == true) || (renderer?.hasPendingChanges() == true)
|
||||
val shouldPrompt = !accepted && hasPending
|
||||
|
||||
if (hasPending) {
|
||||
if (shouldPrompt) {
|
||||
val result = Messages.showYesNoDialog(
|
||||
project,
|
||||
"You have pending changes that will be lost. Do you want to close anyway?",
|
||||
"Pending Changes",
|
||||
"Close Anyway",
|
||||
"Cancel",
|
||||
CodeGPTBundle.get("inlineEdit.closeWarning.message"),
|
||||
CodeGPTBundle.get("inlineEdit.closeWarning.title"),
|
||||
CodeGPTBundle.get("inlineEdit.closeWarning.closeAnyway"),
|
||||
CodeGPTBundle.get("shared.cancel"),
|
||||
Messages.getWarningIcon()
|
||||
)
|
||||
if (result != Messages.YES) {
|
||||
return@runInEdt
|
||||
}
|
||||
|
||||
invokeLater {
|
||||
submissionHandler.handleReject()
|
||||
}
|
||||
invokeLater { submissionHandler.handleReject() }
|
||||
}
|
||||
|
||||
logger.debug("Closing inline edit inlay")
|
||||
|
|
@ -274,6 +522,10 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
serviceScope.cancel()
|
||||
inlayDisposable?.dispose()
|
||||
editor.putUserData(INLAY_KEY, null)
|
||||
askResponseBody = null
|
||||
askContainer = null
|
||||
askPopup?.cancel()
|
||||
askPopup = null
|
||||
}
|
||||
|
||||
fun openOrCreateChatFromSession(sessionConversation: Conversation) {
|
||||
|
|
@ -288,10 +540,12 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
val newConversation = Conversation().apply {
|
||||
title = sessionConversation.title
|
||||
projectPath = sessionConversation.projectPath
|
||||
setMessages(sessionConversation.messages)
|
||||
messages = sessionConversation.messages
|
||||
}
|
||||
ee.carlrobert.codegpt.conversations.ConversationService.getInstance().addConversation(newConversation)
|
||||
ee.carlrobert.codegpt.conversations.ConversationService.getInstance().saveConversation(newConversation)
|
||||
ee.carlrobert.codegpt.conversations.ConversationService.getInstance()
|
||||
.addConversation(newConversation)
|
||||
ee.carlrobert.codegpt.conversations.ConversationService.getInstance()
|
||||
.saveConversation(newConversation)
|
||||
openedChatConversation = newConversation
|
||||
project.service<ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager>()
|
||||
.displayConversation(newConversation)
|
||||
|
|
@ -315,7 +569,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
userInputPanel.setInlineEditControlsVisible(visible)
|
||||
}
|
||||
|
||||
fun setThinkingVisible(visible: Boolean, text: String = "Thinking…") {
|
||||
fun setThinkingVisible(visible: Boolean, text: String = CodeGPTBundle.get("shared.thinking")) {
|
||||
userInputPanel.setThinkingVisible(visible, text)
|
||||
}
|
||||
|
||||
|
|
@ -460,7 +714,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
try {
|
||||
val currentHeight = userInputPanel.height
|
||||
val preferredHeight = userInputPanel.preferredSize.height
|
||||
val minHeight = 80
|
||||
val minHeight = 60
|
||||
|
||||
logger.debug("Current sizing - Height: $currentHeight, Preferred: $preferredHeight")
|
||||
|
||||
|
|
@ -539,6 +793,7 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
when (it) {
|
||||
is FileTagDetails -> it.virtualFile
|
||||
is EditorTagDetails -> it.virtualFile
|
||||
is FolderTagDetails -> it.folder // TODO
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -565,6 +820,4 @@ class InlineEditInlay(private var editor: Editor) : Disposable {
|
|||
processor.process(Message("", ""), stringBuilder)
|
||||
return stringBuilder.toString().takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ class InlineEditInlayRenderer(
|
|||
|
||||
private val hunkUIs = mutableListOf<HunkUI>()
|
||||
|
||||
fun hasPendingChanges(): Boolean {
|
||||
val session = editor.getUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_SESSION)
|
||||
if (session != null) return session.hasPendingHunks()
|
||||
val anyPendingChanges = changes.any { !it.isAccepted && !it.isRejected }
|
||||
val anyPendingHunks = hunkUIs.any { !it.hunk.accepted && !it.hunk.rejected }
|
||||
return anyPendingChanges || anyPendingHunks
|
||||
}
|
||||
|
||||
fun renderHunks(hunks: List<InlineEditSession.Hunk>) {
|
||||
runInEdt {
|
||||
hunks.forEach { renderHunk(it) }
|
||||
|
|
@ -481,9 +489,40 @@ class InlineEditInlayRenderer(
|
|||
session.acceptAll()
|
||||
return
|
||||
}
|
||||
val changesToAccept = changes.filter { !it.isAccepted && !it.isRejected }
|
||||
val changesToAccept = changes
|
||||
.filter { !it.isAccepted && !it.isRejected }
|
||||
.sortedByDescending { it.startOffset }
|
||||
changesToAccept.forEach { acceptChange(it) }
|
||||
if (changesToAccept.isEmpty()) return
|
||||
|
||||
// Apply all pending changes in a single write command to avoid
|
||||
// interleaved recomputation and multiple command entries.
|
||||
WriteCommandAction.runWriteCommandAction(
|
||||
project,
|
||||
ee.carlrobert.codegpt.CodeGPTBundle.get("inlineEdit.undo.acceptAll.commandTitle"),
|
||||
ee.carlrobert.codegpt.CodeGPTBundle.get("inlineEdit.undo.commandGroup"),
|
||||
{
|
||||
changesToAccept.forEach { change ->
|
||||
try {
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
?.markChangesAsAccepted()
|
||||
editor.document.replaceString(
|
||||
change.startOffset,
|
||||
change.endOffset,
|
||||
change.newText
|
||||
)
|
||||
change.isAccepted = true
|
||||
removeChangeVisuals(change)
|
||||
} catch (e: Exception) {
|
||||
logger.debug("Error accepting change during bulk apply", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (changes.isEmpty()) {
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
?.setInlineEditControlsVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun rejectAll() {
|
||||
|
|
|
|||
|
|
@ -338,9 +338,9 @@ class InlineEditSearchReplaceListener(
|
|||
val hadChanges = showFinalDiff()
|
||||
|
||||
val inlay = editor.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
inlay?.observableProperties?.hasPendingChanges?.set(hadChanges)
|
||||
inlay?.setThinkingVisible(false)
|
||||
inlay?.setInlineEditControlsVisible(hadChanges)
|
||||
inlay?.hideAskPopup()
|
||||
inlay?.onCompletionFinished()
|
||||
|
||||
val statusComponent = (editor.scrollPane as JBScrollPane).statusComponent
|
||||
|
|
@ -1015,10 +1015,6 @@ class InlineEditSearchReplaceListener(
|
|||
}
|
||||
}
|
||||
|
||||
fun showHint(message: String) {
|
||||
showInlineHint(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LISTENER_KEY =
|
||||
Key.create<InlineEditSearchReplaceListener>("InlineEditSearchReplaceListener")
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ import com.intellij.openapi.application.runReadAction
|
|||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.editor.RangeMarker
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.keymap.KeymapManager
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
|
|
@ -70,7 +68,6 @@ class InlineEditSession(
|
|||
hunks.clear()
|
||||
hunks.addAll(newHunks)
|
||||
renderer.renderHunks(hunks)
|
||||
updateHasPendingChangesFlag()
|
||||
}
|
||||
|
||||
private fun computeHunks(): List<Hunk> {
|
||||
|
|
@ -145,7 +142,6 @@ class InlineEditSession(
|
|||
hunks.addAll(newHunks)
|
||||
renderer.setInteractive(interactive)
|
||||
renderer.replaceHunks(hunks)
|
||||
updateHasPendingChangesFlag()
|
||||
}
|
||||
|
||||
fun acceptNearestToCaret() {
|
||||
|
|
@ -166,16 +162,20 @@ class InlineEditSession(
|
|||
|
||||
fun acceptAll() {
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)?.markChangesAsAccepted()
|
||||
hunks
|
||||
.filter { !it.accepted && !it.rejected }
|
||||
.sortedByDescending { it.baseMarker.startOffset }
|
||||
.forEach { acceptHunk(it) }
|
||||
removeCompareLinkIfAny()
|
||||
clearAcceptedHunks()
|
||||
}
|
||||
|
||||
private fun clearAcceptedHunks() {
|
||||
hunks.removeIf { it.accepted }
|
||||
WriteCommandAction.runWriteCommandAction(project) {
|
||||
val start = rootMarker.startOffset.coerceAtLeast(0)
|
||||
val end = rootMarker.endOffset.coerceAtLeast(start).coerceAtMost(editor.document.textLength)
|
||||
editor.document.replaceString(start, end, proposedText)
|
||||
}
|
||||
|
||||
hunks.clear()
|
||||
lockedRanges.clear()
|
||||
rejectedRanges.clear()
|
||||
renderer.replaceHunks(emptyList())
|
||||
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)?.setInlineEditControlsVisible(false)
|
||||
removeCompareLinkIfAny()
|
||||
}
|
||||
|
||||
fun rejectAll() {
|
||||
|
|
@ -186,7 +186,10 @@ class InlineEditSession(
|
|||
editor.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
?.restorePreviousPrompt()
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)?.let {
|
||||
try { it.dispose() } catch (_: Exception) {}
|
||||
try {
|
||||
it.dispose()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
editor.putUserData(InlineEditInlay.INLAY_KEY, null)
|
||||
}
|
||||
dispose()
|
||||
|
|
@ -216,7 +219,6 @@ class InlineEditSession(
|
|||
removeCompareLinkIfAny()
|
||||
}
|
||||
}
|
||||
updateHasPendingChangesFlag()
|
||||
}
|
||||
|
||||
private fun rejectHunk(hunk: Hunk) {
|
||||
|
|
@ -244,7 +246,6 @@ class InlineEditSession(
|
|||
?.setInlineEditControlsVisible(false)
|
||||
removeCompareLinkIfAny()
|
||||
}
|
||||
updateHasPendingChangesFlag()
|
||||
}
|
||||
|
||||
fun accept(hunk: Hunk) = acceptHunk(hunk)
|
||||
|
|
@ -255,14 +256,8 @@ class InlineEditSession(
|
|||
return hunks.any { !it.accepted && !it.rejected }
|
||||
}
|
||||
|
||||
private fun updateHasPendingChangesFlag() {
|
||||
editor.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
?.observableProperties
|
||||
?.hasPendingChanges
|
||||
?.set(hasPendingHunks())
|
||||
}
|
||||
|
||||
private fun rangesOverlap(aStart: Int, aEnd: Int, bStart: Int, bEnd: Int): Boolean {
|
||||
if (aStart == aEnd && bStart == bEnd) return aStart == bStart
|
||||
val start = maxOf(aStart, bStart)
|
||||
val end = minOf(aEnd, bEnd)
|
||||
return start < end
|
||||
|
|
|
|||
|
|
@ -12,12 +12,16 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
|
|||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.codecompletions.CompletionProgressNotifier
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestService
|
||||
import ee.carlrobert.codegpt.completions.InlineEditCompletionParameters
|
||||
import ee.carlrobert.codegpt.completions.*
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil
|
||||
import okhttp3.sse.EventSource
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
|
@ -33,28 +37,149 @@ class InlineEditSubmissionHandler(
|
|||
private val currentEventSourceRef = AtomicReference<EventSource?>(null)
|
||||
private val logger = Logger.getInstance(InlineEditSubmissionHandler::class.java)
|
||||
|
||||
fun getSessionConversation(): Conversation = sessionConversation
|
||||
|
||||
fun handleSubmit(
|
||||
userPrompt: String,
|
||||
referencedFiles: List<ReferencedFile>? = null,
|
||||
gitDiff: String? = null,
|
||||
conversationHistory: List<Conversation>? = null,
|
||||
diagnosticsInfo: String? = null
|
||||
diagnosticsInfo: String? = null,
|
||||
forceApply: Boolean = false
|
||||
) {
|
||||
editor.project?.let {
|
||||
CompletionProgressNotifier.Companion.update(it, true)
|
||||
}
|
||||
|
||||
observableProperties.loading.set(true)
|
||||
observableProperties.submitted.set(true)
|
||||
|
||||
previousPromptRef.getAndSet(userPrompt)
|
||||
previousSourceRef.getAndSet(editor.document.text)
|
||||
|
||||
runInEdt { editor.selectionModel.removeSelection() }
|
||||
prepareSubmission(userPrompt)
|
||||
|
||||
val file = FileDocumentManager.getInstance().getFile(editor.document)
|
||||
val editorEx = editor as? EditorEx ?: return
|
||||
val inlay = editorEx.getUserData(InlineEditInlay.INLAY_KEY) ?: return
|
||||
val selectedService =
|
||||
ModelSelectionService.getInstance().getServiceForFeature(FeatureType.INLINE_EDIT)
|
||||
|
||||
if (!forceApply && inlay.isQuickQuestionEnabled()) {
|
||||
handleAskSubmission(
|
||||
userPrompt,
|
||||
referencedFiles,
|
||||
conversationHistory,
|
||||
editorEx,
|
||||
inlay,
|
||||
file,
|
||||
selectedService
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
handleInlineEditSubmission(
|
||||
userPrompt,
|
||||
referencedFiles,
|
||||
gitDiff,
|
||||
conversationHistory,
|
||||
diagnosticsInfo,
|
||||
editorEx,
|
||||
inlay,
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
private fun prepareSubmission(userPrompt: String) {
|
||||
editor.project?.let { CompletionProgressNotifier.update(it, true) }
|
||||
observableProperties.loading.set(true)
|
||||
observableProperties.submitted.set(true)
|
||||
previousPromptRef.getAndSet(userPrompt)
|
||||
previousSourceRef.getAndSet(editor.document.text)
|
||||
runInEdt { editor.selectionModel.removeSelection() }
|
||||
}
|
||||
|
||||
private fun handleAskSubmission(
|
||||
userPrompt: String,
|
||||
referencedFiles: List<ReferencedFile>?,
|
||||
conversationHistory: List<Conversation>?,
|
||||
editorEx: EditorEx,
|
||||
inlay: InlineEditInlay,
|
||||
file: com.intellij.openapi.vfs.VirtualFile?,
|
||||
selectedService: ServiceType
|
||||
) {
|
||||
val message = Message(userPrompt)
|
||||
sessionConversation.addMessage(message)
|
||||
|
||||
inlay.resetAskContainer()
|
||||
|
||||
val withCurrentFile = buildReferencedFilesWithCurrent(file, referencedFiles)
|
||||
|
||||
val params = ChatCompletionParameters
|
||||
.builder(sessionConversation, message)
|
||||
.project(editor.project)
|
||||
.referencedFiles(withCurrentFile)
|
||||
.history(conversationHistory)
|
||||
.chatMode(ChatMode.ASK)
|
||||
.featureType(FeatureType.CHAT)
|
||||
.build()
|
||||
|
||||
sendAskRequest(editorEx, inlay, selectedService, params)
|
||||
}
|
||||
|
||||
private fun buildReferencedFilesWithCurrent(
|
||||
file: com.intellij.openapi.vfs.VirtualFile?,
|
||||
referencedFiles: List<ReferencedFile>?
|
||||
): List<ReferencedFile> {
|
||||
val list = referencedFiles?.toMutableList() ?: mutableListOf()
|
||||
file?.let { vf ->
|
||||
val currentRef = ReferencedFile.from(vf)
|
||||
if (list.none { it.filePath == currentRef.filePath }) {
|
||||
list.add(currentRef)
|
||||
}
|
||||
}
|
||||
return list.toList()
|
||||
}
|
||||
|
||||
private fun sendAskRequest(
|
||||
editorEx: EditorEx,
|
||||
inlay: InlineEditInlay,
|
||||
selectedService: ServiceType,
|
||||
params: ChatCompletionParameters
|
||||
) {
|
||||
try {
|
||||
currentEventSourceRef.getAndSet(null)?.cancel()
|
||||
|
||||
val project = editor.project ?: return
|
||||
val request = CompletionRequestFactory
|
||||
.getFactory(selectedService)
|
||||
.createInlineEditQuestionRequest(params)
|
||||
val listener = ChatCompletionEventListener(
|
||||
project,
|
||||
params,
|
||||
InlineAskResponseListener(project, inlay)
|
||||
)
|
||||
|
||||
val eventSource = service<CompletionRequestService>()
|
||||
.getChatCompletionAsync(request, listener, selectedService)
|
||||
currentEventSourceRef.set(eventSource)
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("InlineAsk: request dispatch failed", ex)
|
||||
runInEdt {
|
||||
OverlayUtil.showNotification(
|
||||
ex.message ?: CodeGPTBundle.get("error.askRequestFailed"),
|
||||
NotificationType.ERROR
|
||||
)
|
||||
observableProperties.loading.set(false)
|
||||
observableProperties.submitted.set(false)
|
||||
editorEx.getUserData(InlineEditInlay.INLAY_KEY)
|
||||
?.setThinkingVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInlineEditSubmission(
|
||||
userPrompt: String,
|
||||
referencedFiles: List<ReferencedFile>?,
|
||||
gitDiff: String?,
|
||||
conversationHistory: List<Conversation>?,
|
||||
diagnosticsInfo: String?,
|
||||
editorEx: EditorEx,
|
||||
inlay: InlineEditInlay,
|
||||
file: com.intellij.openapi.vfs.VirtualFile?
|
||||
) {
|
||||
sessionConversation.addMessage(Message(userPrompt))
|
||||
|
||||
val parameters = InlineEditCompletionParameters(
|
||||
userPrompt,
|
||||
runReadAction { editor.selectionModel.selectedText },
|
||||
|
|
@ -85,13 +210,19 @@ class InlineEditSubmissionHandler(
|
|||
|
||||
editorEx.putUserData(InlineEditSearchReplaceListener.LISTENER_KEY, listener)
|
||||
|
||||
listener.showHint("Submitting inline edit…")
|
||||
|
||||
editorEx.getUserData(InlineEditInlay.INLAY_KEY)?.apply {
|
||||
inlay.apply {
|
||||
setInlineEditControlsVisible(false)
|
||||
setThinkingVisible(true)
|
||||
}
|
||||
|
||||
sendInlineEditRequest(editorEx, parameters, listener)
|
||||
}
|
||||
|
||||
private fun sendInlineEditRequest(
|
||||
editorEx: EditorEx,
|
||||
parameters: InlineEditCompletionParameters,
|
||||
listener: InlineEditSearchReplaceListener
|
||||
) {
|
||||
try {
|
||||
currentEventSourceRef.getAndSet(null)?.cancel()
|
||||
|
||||
|
|
@ -100,12 +231,11 @@ class InlineEditSubmissionHandler(
|
|||
listener
|
||||
)
|
||||
currentEventSourceRef.set(eventSource)
|
||||
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("InlineEdit: request dispatch failed", ex)
|
||||
runInEdt {
|
||||
OverlayUtil.showNotification(
|
||||
ex.message ?: "Inline Edit request failed",
|
||||
ex.message ?: CodeGPTBundle.get("error.inlineEditRequestFailed"),
|
||||
NotificationType.ERROR
|
||||
)
|
||||
observableProperties.loading.set(false)
|
||||
|
|
@ -123,16 +253,15 @@ class InlineEditSubmissionHandler(
|
|||
runInEdt {
|
||||
editorEx.getUserData(InlineEditInlay.INLAY_KEY)?.setThinkingVisible(false)
|
||||
|
||||
val existingListener = editorEx.getUserData(InlineEditSearchReplaceListener.LISTENER_KEY)
|
||||
val existingListener =
|
||||
editorEx.getUserData(InlineEditSearchReplaceListener.LISTENER_KEY)
|
||||
existingListener?.stopGenerating()
|
||||
|
||||
val session = editorEx.getUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_SESSION)
|
||||
if (session != null && session.hasPendingHunks()) {
|
||||
editorEx.getUserData(InlineEditInlay.INLAY_KEY)?.setInlineEditControlsVisible(true)
|
||||
observableProperties.hasPendingChanges.set(true)
|
||||
} else {
|
||||
editorEx.getUserData(InlineEditInlay.INLAY_KEY)?.setInlineEditControlsVisible(false)
|
||||
observableProperties.hasPendingChanges.set(false)
|
||||
}
|
||||
|
||||
observableProperties.loading.set(false)
|
||||
|
|
@ -155,7 +284,8 @@ class InlineEditSubmissionHandler(
|
|||
restorePreviousPrompt()
|
||||
runInEdt {
|
||||
val editorEx = editor as? EditorEx
|
||||
val existingListener = editorEx?.getUserData(InlineEditSearchReplaceListener.LISTENER_KEY)
|
||||
val existingListener =
|
||||
editorEx?.getUserData(InlineEditSearchReplaceListener.LISTENER_KEY)
|
||||
existingListener?.dispose()
|
||||
editorEx?.getUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_SESSION)?.dispose()
|
||||
editorEx?.putUserData(CodeGPTKeys.EDITOR_INLINE_EDIT_SESSION, null)
|
||||
|
|
@ -165,7 +295,6 @@ class InlineEditSubmissionHandler(
|
|||
|
||||
observableProperties.loading.set(false)
|
||||
observableProperties.submitted.set(false)
|
||||
observableProperties.hasPendingChanges.set(false)
|
||||
editor.project?.let { project ->
|
||||
CompletionProgressNotifier.Companion.update(project, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import ee.carlrobert.codegpt.inlineedit.InlineEditInlay
|
||||
import ee.carlrobert.codegpt.inlineedit.InlineEditSubmissionHandler
|
||||
|
||||
data class ApplyContext(
|
||||
val editor: EditorEx,
|
||||
val inlay: InlineEditInlay,
|
||||
val submissionHandler: InlineEditSubmissionHandler,
|
||||
val promptText: String,
|
||||
val lastAssistantResponse: String
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
interface ApplyStrategy {
|
||||
fun apply(ctx: ApplyContext)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
|
||||
object InlineEditApplyStrategyFactory {
|
||||
fun get(): ApplyStrategy {
|
||||
val serviceType = ModelSelectionService.getInstance().getServiceForFeature(FeatureType.INLINE_EDIT)
|
||||
return if (serviceType == ServiceType.PROXYAI) ProxyAIApplyStrategy() else SearchReplaceApplyStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
interface InlineEditEngine {
|
||||
fun apply(ctx: ApplyContext)
|
||||
}
|
||||
|
||||
class InlineEditEngineImpl : InlineEditEngine {
|
||||
override fun apply(ctx: ApplyContext) {
|
||||
InlineEditApplyStrategyFactory.get().apply(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import ee.carlrobert.codegpt.completions.CompletionClientProvider
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.inlineedit.InlineEditSession
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
import ee.carlrobert.llm.client.codegpt.request.AutoApplyRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import ee.carlrobert.codegpt.util.MarkdownUtil
|
||||
|
||||
class ProxyAIApplyStrategy : ApplyStrategy {
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun apply(ctx: ApplyContext) {
|
||||
val blocks = MarkdownUtil.extractCodeBlocks(ctx.lastAssistantResponse)
|
||||
if (blocks.isEmpty()) return
|
||||
|
||||
val modelSelection =
|
||||
ModelSelectionService.getInstance().getModelSelectionForFeature(FeatureType.AUTO_APPLY)
|
||||
if (modelSelection.provider != ServiceType.PROXYAI) return
|
||||
|
||||
val updateSnippet = blocks.joinToString("\n// ... existing code ...\n\n") { it.trimEnd() }
|
||||
val original = ctx.editor.document.text
|
||||
|
||||
coroutineScope.launch {
|
||||
runInEdt {
|
||||
ctx.inlay.setThinkingVisible(true, CodeGPTBundle.get("inlineEdit.applying"))
|
||||
}
|
||||
|
||||
val merged = try {
|
||||
CompletionClientProvider.getCodeGPTClient()
|
||||
.applyChanges(AutoApplyRequest(modelSelection.model, original, updateSnippet))
|
||||
.mergedCode
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (merged.isNullOrBlank()) {
|
||||
runInEdt {
|
||||
ctx.inlay.setThinkingVisible(false)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
runInEdt {
|
||||
val baseRange = TextRange(0, ctx.editor.document.textLength)
|
||||
InlineEditSession.start(
|
||||
requireNotNull(ctx.editor.project),
|
||||
ctx.editor,
|
||||
baseRange,
|
||||
merged
|
||||
)
|
||||
ctx.inlay.setInlineEditControlsVisible(true)
|
||||
ctx.inlay.setThinkingVisible(false)
|
||||
ctx.inlay.hideAskPopup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package ee.carlrobert.codegpt.inlineedit.engine
|
||||
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import ee.carlrobert.codegpt.inlineedit.InlineEditSearchReplaceListener
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestService
|
||||
import ee.carlrobert.codegpt.completions.InlineEditCompletionParameters
|
||||
import com.intellij.openapi.components.service
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
|
||||
class SearchReplaceApplyStrategy : ApplyStrategy {
|
||||
override fun apply(ctx: ApplyContext) {
|
||||
val editor = ctx.editor
|
||||
val inlay = ctx.inlay
|
||||
|
||||
runInEdt {
|
||||
inlay.setInlineEditControlsVisible(false)
|
||||
inlay.setThinkingVisible(true)
|
||||
}
|
||||
|
||||
val file = editor.virtualFile
|
||||
val parameters = InlineEditCompletionParameters(
|
||||
ctx.promptText,
|
||||
editor.selectionModel.selectedText,
|
||||
file?.path,
|
||||
file?.extension,
|
||||
editor.project?.basePath,
|
||||
null,
|
||||
null,
|
||||
ctx.submissionHandler.getSessionConversation(),
|
||||
null,
|
||||
null,
|
||||
editor.caretModel.offset
|
||||
)
|
||||
|
||||
val requestId = System.nanoTime()
|
||||
editor.putUserData(InlineEditSearchReplaceListener.REQUEST_ID_KEY, requestId)
|
||||
|
||||
val listener = InlineEditSearchReplaceListener(
|
||||
editor,
|
||||
inlay.observableProperties,
|
||||
TextRange(editor.selectionModel.selectionStart, editor.selectionModel.selectionEnd),
|
||||
requestId,
|
||||
ctx.submissionHandler.getSessionConversation()
|
||||
)
|
||||
editor.putUserData(InlineEditSearchReplaceListener.LISTENER_KEY, listener)
|
||||
|
||||
try {
|
||||
service<CompletionRequestService>().getInlineEditCompletionAsync(parameters, listener)
|
||||
} catch (_: Exception) {
|
||||
runInEdt {
|
||||
inlay.setThinkingVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,4 +61,5 @@ class CodeCompletionSettingsState : BaseState() {
|
|||
var collectDependencyStructure by property(true)
|
||||
var contextAwareEnabled by property(false)
|
||||
var psiStructureAnalyzeDepth by property(2)
|
||||
var myAwesomeFeatureEnabled by property(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ 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.ui.JBColor
|
||||
import com.intellij.util.application
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
|
|
@ -46,11 +47,13 @@ import ee.carlrobert.codegpt.util.EditorUtil
|
|||
import ee.carlrobert.llm.client.codegpt.request.AutoApplyRequest
|
||||
import ee.carlrobert.llm.client.codegpt.response.CodeGPTException
|
||||
import java.util.regex.Pattern
|
||||
import javax.swing.BorderFactory
|
||||
|
||||
class ResponseEditorPanel(
|
||||
private val project: Project,
|
||||
item: Segment,
|
||||
readOnly: Boolean,
|
||||
private val compact: Boolean,
|
||||
disposableParent: Disposable,
|
||||
) : BorderLayoutPanel(), Disposable {
|
||||
|
||||
|
|
@ -67,17 +70,29 @@ class ResponseEditorPanel(
|
|||
private var searchReplaceHandler: SearchReplaceHandler
|
||||
|
||||
init {
|
||||
border = JBUI.Borders.empty(8, 0)
|
||||
isOpaque = false
|
||||
if (compact) {
|
||||
val visibleBorderColor = JBColor.namedColor("Component.borderColor", JBColor(0xC4C9D0, 0x44484F))
|
||||
border = BorderFactory.createCompoundBorder(
|
||||
JBUI.Borders.customLine(visibleBorderColor, 1),
|
||||
JBUI.Borders.empty(4, 6)
|
||||
)
|
||||
} else {
|
||||
border = JBUI.Borders.empty(8, 0)
|
||||
}
|
||||
|
||||
val state = stateManager.createFromSegment(item, readOnly)
|
||||
val state = stateManager.createFromSegment(item, readOnly, compact = compact)
|
||||
val editor = state.editor
|
||||
configureEditor(editor)
|
||||
searchReplaceHandler = SearchReplaceHandler(stateManager) { oldEditor, newEditor ->
|
||||
replaceEditor(oldEditor, newEditor)
|
||||
}
|
||||
|
||||
addToCenter(editor.component)
|
||||
if (compact && editor.editorKind != EditorKind.DIFF) {
|
||||
addToCenter(createCompactEditorContainer(editor))
|
||||
} else {
|
||||
addToCenter(editor.component)
|
||||
}
|
||||
updateEditorUI()
|
||||
|
||||
Disposer.register(disposableParent, this)
|
||||
|
|
@ -103,7 +118,11 @@ class ResponseEditorPanel(
|
|||
removeAll()
|
||||
|
||||
configureEditor(newEditor)
|
||||
addToCenter(newEditor.component)
|
||||
if (compact && newEditor.editorKind != EditorKind.DIFF) {
|
||||
addToCenter(createCompactEditorContainer(newEditor))
|
||||
} else {
|
||||
addToCenter(newEditor.component)
|
||||
}
|
||||
|
||||
ComponentFactory.updateEditorPreferredSize(newEditor, expanded)
|
||||
updateEditorUI()
|
||||
|
|
@ -115,7 +134,7 @@ class ResponseEditorPanel(
|
|||
|
||||
fun replaceEditorWithSegment(segment: Segment) {
|
||||
val oldEditor = stateManager.getCurrentState()?.editor ?: return
|
||||
val newState = stateManager.createFromSegment(segment)
|
||||
val newState = stateManager.createFromSegment(segment, compact = compact)
|
||||
replaceEditor(oldEditor, newState.editor)
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +166,12 @@ class ResponseEditorPanel(
|
|||
}
|
||||
|
||||
if (!response.isNullOrBlank()) {
|
||||
stateManager.transitionToDiffState(originalCode, response, params.destination, params.source)
|
||||
stateManager.transitionToDiffState(
|
||||
originalCode,
|
||||
response,
|
||||
params.destination,
|
||||
params.source
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to apply changes", e)
|
||||
|
|
@ -224,8 +248,14 @@ class ResponseEditorPanel(
|
|||
val containsText = currentText.contains(segment.search.trim())
|
||||
|
||||
val newState = if (containsText) {
|
||||
val finalSegment = createReplaceWaitingSegment(searchContent, replaceContent, virtualFile)
|
||||
stateManager.createFromSegment(finalSegment, readOnly = false, eventSource = null, originalSuggestion = replaceContent)
|
||||
val finalSegment =
|
||||
createReplaceWaitingSegment(searchContent, replaceContent, virtualFile)
|
||||
stateManager.createFromSegment(
|
||||
finalSegment,
|
||||
readOnly = false,
|
||||
eventSource = null,
|
||||
originalSuggestion = replaceContent
|
||||
)
|
||||
} else {
|
||||
stateManager.transitionToFailedDiffState(
|
||||
segment.search,
|
||||
|
|
@ -277,11 +307,42 @@ class ResponseEditorPanel(
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (compact && editor.editorKind != EditorKind.DIFF) {
|
||||
editor.settings.apply {
|
||||
isLineNumbersShown = false
|
||||
isLineMarkerAreaShown = false
|
||||
additionalLinesCount = 0
|
||||
additionalColumnsCount = 0
|
||||
isAdditionalPageAtBottom = false
|
||||
isUseSoftWraps = false
|
||||
}
|
||||
editor.gutterComponentEx.apply {
|
||||
isVisible = false
|
||||
parent.isVisible = false
|
||||
}
|
||||
editor.component.border = JBUI.Borders.empty(0, 0)
|
||||
editor.scrollPane.border = JBUI.Borders.empty(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCompactEditorContainer(editor: EditorEx): javax.swing.JComponent {
|
||||
val container = BorderLayoutPanel().apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty()
|
||||
}
|
||||
val inner = BorderLayoutPanel().apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(0)
|
||||
addToCenter(editor.component)
|
||||
}
|
||||
container.add(inner, java.awt.BorderLayout.CENTER)
|
||||
return container
|
||||
}
|
||||
|
||||
private fun updateEditorUI() {
|
||||
updateEditorHeightAndUI()
|
||||
updateExpandLinkVisibility()
|
||||
if (!compact) updateExpandLinkVisibility()
|
||||
}
|
||||
|
||||
private fun updateEditorHeightAndUI() {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.intellij.openapi.vfs.VirtualFile
|
|||
import com.intellij.openapi.vfs.readText
|
||||
import com.intellij.ui.components.AnActionLink
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType.INCEPTION
|
||||
|
|
@ -24,9 +25,13 @@ class AutoApplyAction(
|
|||
private val filePath: String?,
|
||||
private val virtualFile: VirtualFile?,
|
||||
private val onApply: (AnActionLink) -> Unit,
|
||||
) : CustomComponentAction, AnAction("Apply", "Apply changes to the editor", AllIcons.Actions.Execute) {
|
||||
) : CustomComponentAction, AnAction(
|
||||
CodeGPTBundle.get("shared.apply"),
|
||||
CodeGPTBundle.get("inlineEdit.apply.description"),
|
||||
AllIcons.Actions.Execute
|
||||
) {
|
||||
|
||||
private val anActionLink: AnActionLink = AnActionLink("Apply", this).apply {
|
||||
private val anActionLink: AnActionLink = AnActionLink(CodeGPTBundle.get("shared.apply"), this).apply {
|
||||
icon = AllIcons.Actions.Execute
|
||||
border = JBUI.Borders.empty(0, 4)
|
||||
}
|
||||
|
|
@ -37,14 +42,14 @@ class AutoApplyAction(
|
|||
|
||||
override fun update(e: AnActionEvent) {
|
||||
if (virtualFile != null) {
|
||||
anActionLink.text = "Apply"
|
||||
anActionLink.text = CodeGPTBundle.get("shared.apply")
|
||||
anActionLink.isEnabled = true
|
||||
anActionLink.toolTipText = "Apply changes to ${virtualFile.name}"
|
||||
anActionLink.toolTipText = CodeGPTBundle.get("inlineEdit.apply.changesTo", virtualFile.name)
|
||||
|
||||
if (EditorUtil.getFileContent(virtualFile).trim() == toolwindowEditor.document.text.trim()) {
|
||||
anActionLink.isEnabled = false
|
||||
anActionLink.isVisible = true
|
||||
anActionLink.toolTipText = "No changes to apply"
|
||||
anActionLink.toolTipText = CodeGPTBundle.get("inlineEdit.apply.noChanges")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -53,7 +58,7 @@ class AutoApplyAction(
|
|||
val selectedEditorFile = selectedEditor?.virtualFile
|
||||
val canApply = selectedEditorFile != null && selectedEditorFile.isWritable
|
||||
|
||||
anActionLink.text = if (canApply) "Apply to ${selectedEditorFile.name}" else "Apply"
|
||||
anActionLink.text = if (canApply) CodeGPTBundle.get("inlineEdit.apply.toFile", selectedEditorFile.name) else CodeGPTBundle.get("shared.apply")
|
||||
anActionLink.isEnabled = canApply
|
||||
anActionLink.isVisible = canApply
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,25 @@ class EditorStateManager(private val project: Project) {
|
|||
private var currentState: EditorState? = null
|
||||
private var diffEditorManager: DiffEditorManager? = null
|
||||
|
||||
fun createFromSegment(segment: Segment, readOnly: Boolean = false, eventSource: EventSource? = null, originalSuggestion: String? = null): EditorState {
|
||||
fun createFromSegment(
|
||||
segment: Segment,
|
||||
readOnly: Boolean = false,
|
||||
eventSource: EventSource? = null,
|
||||
originalSuggestion: String? = null,
|
||||
compact: Boolean = false
|
||||
): EditorState {
|
||||
val editor = EditorFactory.createEditor(project, segment)
|
||||
val state = if (editor.editorKind == EditorKind.DIFF) {
|
||||
createDiffState(editor, segment, eventSource, originalSuggestion)
|
||||
} else {
|
||||
RegularEditorState(editor, segment, project)
|
||||
}
|
||||
|
||||
runInEdt {
|
||||
val headerComponent = state.createHeaderComponent(readOnly)
|
||||
EditorFactory.configureEditor(editor, headerComponent)
|
||||
|
||||
if (!compact) {
|
||||
runInEdt {
|
||||
val headerComponent = state.createHeaderComponent(readOnly)
|
||||
EditorFactory.configureEditor(editor, headerComponent)
|
||||
}
|
||||
}
|
||||
|
||||
RESPONSE_EDITOR_STATE_KEY.set(editor, state)
|
||||
|
|
@ -79,7 +87,9 @@ class EditorStateManager(private val project: Project) {
|
|||
diffViewer.rediff(true)
|
||||
}
|
||||
(oldEditor.component.parent as? ResponseEditorPanel)?.replaceEditor(oldEditor, editor)
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.updateDiffStats(diffViewer.diffChanges ?: emptyList())
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.updateDiffStats(
|
||||
diffViewer.diffChanges ?: emptyList()
|
||||
)
|
||||
(editor.permanentHeaderComponent as? DiffHeaderPanel)?.handleDone()
|
||||
}
|
||||
}
|
||||
|
|
@ -122,12 +132,17 @@ class EditorStateManager(private val project: Project) {
|
|||
currentState = null
|
||||
}
|
||||
|
||||
private fun createDiffState(editor: EditorEx, segment: Segment, eventSource: EventSource? = null, originalSuggestion: String? = null): EditorState {
|
||||
private fun createDiffState(
|
||||
editor: EditorEx,
|
||||
segment: Segment,
|
||||
eventSource: EventSource? = null,
|
||||
originalSuggestion: String? = null
|
||||
): EditorState {
|
||||
val virtualFile = getVirtualFile(segment.filePath)
|
||||
val diffViewer = RESPONSE_EDITOR_DIFF_VIEWER_KEY.get(editor)
|
||||
val diffEditorManager = DiffEditorManager(project, diffViewer)
|
||||
this.diffEditorManager = diffEditorManager
|
||||
|
||||
|
||||
val state = StandardDiffEditorState(
|
||||
editor,
|
||||
segment,
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class UserMessagePanel(
|
|||
|
||||
private fun setupResponseBody() {
|
||||
addContent(
|
||||
ChatMessageResponseBody(project, true, false, false, false, parentDisposable)
|
||||
ChatMessageResponseBody(project, true, false, false, false, false, parentDisposable)
|
||||
.withResponse(message.prompt)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.intellij.ui.JBColor
|
|||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.util.MarkdownUtil
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.ItemEvent
|
||||
import javax.swing.*
|
||||
|
|
@ -30,7 +31,7 @@ class ThoughtProcessPanel : JPanel(BorderLayout()) {
|
|||
fun setFinished() {
|
||||
if (finished) return
|
||||
|
||||
toggleButton.text = "Thought Process"
|
||||
toggleButton.text = CodeGPTBundle.get("thoughtProcess.title")
|
||||
toggleButton.isSelected = false
|
||||
finished = true
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ class ThoughtProcessPanel : JPanel(BorderLayout()) {
|
|||
}
|
||||
|
||||
private fun createToggleButton() =
|
||||
JToggleButton("Thinking...", AllIcons.General.ArrowUp, true).apply {
|
||||
JToggleButton(CodeGPTBundle.get("thoughtProcess.thinking"), AllIcons.General.ArrowUp, true).apply {
|
||||
isFocusPainted = false
|
||||
isContentAreaFilled = false
|
||||
background = background
|
||||
|
|
@ -76,4 +77,4 @@ class ThoughtProcessPanel : JPanel(BorderLayout()) {
|
|||
contentPanel.isVisible = e.stateChange == ItemEvent.SELECTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,244 +0,0 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorCustomElementRenderer
|
||||
import com.intellij.openapi.editor.Inlay
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
import com.intellij.openapi.editor.event.EditorMouseEvent
|
||||
import com.intellij.openapi.editor.event.EditorMouseListener
|
||||
import com.intellij.openapi.editor.event.EditorMouseMotionListener
|
||||
import com.intellij.openapi.editor.markup.TextAttributes
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.geom.Rectangle2D
|
||||
import javax.swing.Icon
|
||||
|
||||
class PromptTextFieldInlayRenderer(
|
||||
private val project: Project,
|
||||
private val actionPrefix: String,
|
||||
private val text: String?,
|
||||
private val fileName: String,
|
||||
private val tooltipText: String?,
|
||||
private val onClose: (Inlay<*>) -> Unit
|
||||
) : EditorCustomElementRenderer {
|
||||
|
||||
private val closeIcon = AllIcons.Actions.Close
|
||||
private val helpIcon = AllIcons.General.ContextHelp
|
||||
|
||||
private var tooltip: JBPopup? = null
|
||||
|
||||
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
|
||||
val editor = inlay.editor
|
||||
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
|
||||
val textWidth = editor.component.getFontMetrics(font)
|
||||
.stringWidth(actionPrefix + (if (text != null) ":$text" else ""))
|
||||
|
||||
if (tooltipText.isNullOrEmpty()) {
|
||||
return textWidth + closeIcon.iconWidth + JBUI.scale(10)
|
||||
}
|
||||
|
||||
return textWidth + closeIcon.iconWidth + JBUI.scale(10) + helpIcon.iconWidth + JBUI.scale(10)
|
||||
}
|
||||
|
||||
override fun paint(
|
||||
inlay: Inlay<*>,
|
||||
g: Graphics2D,
|
||||
target: Rectangle2D,
|
||||
textAttributes: TextAttributes
|
||||
) {
|
||||
val editor = inlay.editor
|
||||
val currentTextAttributes: TextAttributes? =
|
||||
EditorColorsManager.getInstance().globalScheme.getAttributes(
|
||||
DefaultLanguageHighlighterColors.INLAY_DEFAULT
|
||||
)
|
||||
|
||||
drawBackground(g, target, currentTextAttributes)
|
||||
drawBorder(g, target)
|
||||
drawText(g, target, editor, currentTextAttributes)
|
||||
drawCloseIcon(g, target)
|
||||
if (tooltipText != null) {
|
||||
drawHelpIcon(g, target)
|
||||
}
|
||||
|
||||
addMouseListeners(editor, inlay, target)
|
||||
}
|
||||
|
||||
private fun drawBackground(
|
||||
g: Graphics2D,
|
||||
target: Rectangle2D,
|
||||
textAttributes: TextAttributes?
|
||||
) {
|
||||
g.color = textAttributes?.backgroundColor ?: JBColor.background()
|
||||
g.fill(target)
|
||||
}
|
||||
|
||||
private fun drawBorder(g: Graphics2D, target: Rectangle2D) {
|
||||
g.color = JBColor.border()
|
||||
g.draw(target)
|
||||
}
|
||||
|
||||
private fun drawText(
|
||||
g: Graphics2D,
|
||||
target: Rectangle2D,
|
||||
editor: Editor,
|
||||
textAttributes: TextAttributes?
|
||||
) {
|
||||
g.font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
|
||||
|
||||
val metrics = g.fontMetrics
|
||||
val textHeight = metrics.height
|
||||
val startX = (target.x + JBUI.scale(5)).toInt()
|
||||
val startY = (target.y + (target.height - textHeight) / 2 + metrics.ascent).toInt()
|
||||
|
||||
g.color = textAttributes?.foregroundColor ?: JBColor.foreground()
|
||||
g.drawString(actionPrefix, startX, startY)
|
||||
|
||||
if (!text.isNullOrEmpty()) {
|
||||
val prefixWidth = metrics.stringWidth(actionPrefix)
|
||||
g.color = service<EditorColorsManager>().globalScheme.defaultForeground
|
||||
g.drawString(":$text", startX + prefixWidth, startY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawCloseIcon(g: Graphics2D, target: Rectangle2D) {
|
||||
val iconX = (target.x + target.width - closeIcon.iconWidth - JBUI.scale(5)).toInt()
|
||||
val iconY = (target.y + (target.height - closeIcon.iconHeight) / 2).toInt()
|
||||
closeIcon.paintIcon(null, g, iconX, iconY)
|
||||
}
|
||||
|
||||
private fun drawHelpIcon(g: Graphics2D, target: Rectangle2D) {
|
||||
val iconX =
|
||||
(target.x + target.width - closeIcon.iconWidth - helpIcon.iconWidth - JBUI.scale(10)).toInt()
|
||||
val iconY = (target.y + (target.height - helpIcon.iconHeight) / 2).toInt()
|
||||
helpIcon.paintIcon(null, g, iconX, iconY)
|
||||
}
|
||||
|
||||
private fun showTooltip(inlay: Inlay<*>) {
|
||||
if (tooltipText != null) {
|
||||
hideTooltip()
|
||||
|
||||
val tooltipContent = CodePreviewTooltipContent(project, fileName, tooltipText)
|
||||
tooltip = JBPopupFactory.getInstance()
|
||||
.createComponentPopupBuilder(
|
||||
tooltipContent,
|
||||
tooltipContent.getFocusableComponent()
|
||||
)
|
||||
.setTitle("Code Preview")
|
||||
.setResizable(true)
|
||||
.setMovable(true)
|
||||
.setStretchToOwnerHeight(true)
|
||||
.setStretchToOwnerWidth(true)
|
||||
.setMinSize(
|
||||
Dimension(
|
||||
tooltipContent.preferredSize?.width ?: 240,
|
||||
(tooltipContent.preferredSize?.height ?: 0)
|
||||
)
|
||||
)
|
||||
.createPopup()
|
||||
tooltip?.show(
|
||||
RelativePoint(
|
||||
inlay.editor.contentComponent,
|
||||
calculatePopupPoint(inlay, tooltipContent)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePopupPoint(
|
||||
inlay: Inlay<*>,
|
||||
tooltipContent: CodePreviewTooltipContent
|
||||
): Point {
|
||||
val visibleArea = inlay.editor.scrollingModel.visibleArea
|
||||
val inlayBounds = inlay.bounds
|
||||
if (inlayBounds != null) {
|
||||
val x = inlayBounds.x
|
||||
val tooltipHeight = tooltipContent.preferredSize?.height ?: 0
|
||||
val y = inlayBounds.y - tooltipHeight
|
||||
return Point(x, y)
|
||||
}
|
||||
return Point(visibleArea.x, visibleArea.y)
|
||||
}
|
||||
|
||||
private fun hideTooltip() {
|
||||
tooltip?.dispose()
|
||||
}
|
||||
|
||||
private fun addMouseListeners(editor: Editor, inlay: Inlay<*>, target: Rectangle2D) {
|
||||
fun isWithinIconBounds(e: MouseEvent, icon: Icon): Boolean {
|
||||
val iconX = when (icon) {
|
||||
closeIcon -> (target.x + target.width - closeIcon.iconWidth - JBUI.scale(5)).toInt()
|
||||
helpIcon -> (target.x + target.width - closeIcon.iconWidth - helpIcon.iconWidth - JBUI.scale(
|
||||
10
|
||||
)).toInt()
|
||||
|
||||
else -> return false
|
||||
}
|
||||
val iconY = (target.y + (target.height - icon.iconHeight) / 2).toInt()
|
||||
return e.x >= iconX && e.x <= iconX + icon.iconWidth &&
|
||||
e.y >= iconY && e.y <= iconY + icon.iconHeight
|
||||
}
|
||||
|
||||
fun updateCursor(event: EditorMouseEvent, inlay: Inlay<*>) {
|
||||
editor.contentComponent.let {
|
||||
if (inlay.isValid) {
|
||||
val inlayBounds = inlay.bounds
|
||||
val mouseX = event.mouseEvent.x.toDouble()
|
||||
val mouseY = event.mouseEvent.y.toDouble()
|
||||
|
||||
if (inlayBounds != null && inlayBounds.contains(mouseX, mouseY)) {
|
||||
it.cursor = Cursor.getDefaultCursor()
|
||||
return
|
||||
}
|
||||
}
|
||||
it.cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
|
||||
}
|
||||
}
|
||||
|
||||
editor.addEditorMouseMotionListener(object : EditorMouseMotionListener {
|
||||
override fun mouseMoved(event: EditorMouseEvent) {
|
||||
findInlayAtMouseEvent(event)?.let {
|
||||
updateCursor(event, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findInlayAtMouseEvent(event: EditorMouseEvent): Inlay<*>? {
|
||||
val offset = editor.logicalPositionToOffset(event.logicalPosition)
|
||||
val inlays = editor.inlayModel.getInlineElementsInRange(offset, offset)
|
||||
return inlays.firstOrNull { inlay ->
|
||||
val inlayBounds = editor.visualPositionToXY(inlay.visualPosition)
|
||||
val mousePoint = event.mouseEvent.point
|
||||
inlayBounds.x <= mousePoint.x && mousePoint.x <= inlayBounds.x + inlay.widthInPixels
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
editor.addEditorMouseListener(object : EditorMouseListener {
|
||||
override fun mouseClicked(event: EditorMouseEvent) {
|
||||
when {
|
||||
isWithinIconBounds(event.mouseEvent, closeIcon) -> {
|
||||
onClose(inlay)
|
||||
event.consume()
|
||||
}
|
||||
|
||||
isWithinIconBounds(event.mouseEvent, helpIcon) -> {
|
||||
showTooltip(inlay)
|
||||
event.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -89,12 +89,12 @@ class SearchReplaceToggleAction(
|
|||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="bottom">
|
||||
<b>${currentMode.displayName} Mode</b>
|
||||
<div class=\"content\">
|
||||
<div class=\"bottom\">
|
||||
<b>${'$'}{currentMode.displayName} Mode</b>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color:#bcbec4;">
|
||||
${getModeDescription(currentMode)}
|
||||
<div style=\"margin-top: 8px; color:#bcbec4;\">
|
||||
${'$'}{getModeDescription(currentMode)}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
@ -117,4 +117,5 @@ class SearchReplaceToggleAction(
|
|||
"<span style=\"color:#b3ae60;\">Coming soon.</span>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
|||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
|
|
@ -29,8 +30,8 @@ import com.intellij.util.ui.components.BorderLayoutPanel
|
|||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.Icons
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.models.ModelRegistry
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.settings.service.ModelSelectionService
|
||||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
|
|
@ -38,6 +39,7 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
|
|||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import ee.carlrobert.codegpt.ui.components.InlineEditChips
|
||||
import ee.carlrobert.codegpt.ui.components.BadgeChip
|
||||
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.UserInputHeaderPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
|
|
@ -64,7 +66,8 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
private val onStop: () -> Unit,
|
||||
private val onAcceptAll: (() -> Unit)? = null,
|
||||
private val onRejectAll: (() -> Unit)? = null,
|
||||
private val showModeSelector: Boolean = true,
|
||||
private val onApply: (() -> Unit)? = null,
|
||||
private val getMarkdownContent: (() -> String)? = null,
|
||||
withRemovableSelectedEditorTag: Boolean = true,
|
||||
) : BorderLayoutPanel() {
|
||||
|
||||
|
|
@ -76,7 +79,6 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
tagManager: TagManager,
|
||||
onSubmit: (String) -> Unit,
|
||||
onStop: () -> Unit,
|
||||
showModeSelector: Boolean,
|
||||
withRemovableSelectedEditorTag: Boolean
|
||||
) : this(
|
||||
project,
|
||||
|
|
@ -88,7 +90,8 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
onStop,
|
||||
null,
|
||||
null,
|
||||
showModeSelector,
|
||||
null,
|
||||
null,
|
||||
withRemovableSelectedEditorTag
|
||||
)
|
||||
|
||||
|
|
@ -96,6 +99,9 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
private const val CORNER_RADIUS = 16
|
||||
}
|
||||
|
||||
private val quickQuestionCheckbox = JBCheckBox(CodeGPTBundle.get("userInput.quickQuestion"), true).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
private var chatMode: ChatMode = ChatMode.ASK
|
||||
private val disposableCoroutineScope = DisposableCoroutineScope()
|
||||
private val promptTextField =
|
||||
|
|
@ -117,11 +123,16 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
tagManager,
|
||||
totalTokensPanel,
|
||||
promptTextField,
|
||||
withRemovableSelectedEditorTag
|
||||
withRemovableSelectedEditorTag,
|
||||
onApply,
|
||||
getMarkdownContent
|
||||
)
|
||||
|
||||
private var footerPanelRef: JPanel? = null
|
||||
|
||||
private val applyChip = onApply?.let { BadgeChip(CodeGPTBundle.get("shared.apply"), InlineEditChips.GREEN, it) }?.apply {
|
||||
isVisible = false
|
||||
}
|
||||
private val acceptChip =
|
||||
InlineEditChips.acceptAll { onAcceptAll?.invoke() }.apply { isVisible = false }
|
||||
private val rejectChip =
|
||||
|
|
@ -129,7 +140,7 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
private var inlineEditControls: List<JComponent> = listOf(acceptChip, rejectChip)
|
||||
|
||||
private val thinkingIcon = AsyncProcessIcon("inline-edit-thinking").apply { isVisible = false }
|
||||
private val thinkingLabel = javax.swing.JLabel("Thinking…").apply {
|
||||
private val thinkingLabel = javax.swing.JLabel(CodeGPTBundle.get("shared.thinking")).apply {
|
||||
foreground = service<EditorColorsManager>().globalScheme.defaultForeground
|
||||
isVisible = false
|
||||
}
|
||||
|
|
@ -177,11 +188,10 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
val text: String
|
||||
get() = promptTextField.text
|
||||
|
||||
fun isQuickQuestionEnabled(): Boolean = quickQuestionCheckbox.isSelected
|
||||
fun getChatMode(): ChatMode = chatMode
|
||||
fun setChatMode(mode: ChatMode) { chatMode = mode }
|
||||
|
||||
fun setChatMode(mode: ChatMode) {
|
||||
chatMode = mode
|
||||
}
|
||||
|
||||
init {
|
||||
setupDisposables(parentDisposable)
|
||||
|
|
@ -450,13 +460,11 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
}
|
||||
modelComboBoxComponent = modelComboBox
|
||||
|
||||
val searchReplaceToggle = if (showModeSelector) {
|
||||
val searchReplaceToggle = if (featureType == FeatureType.CHAT) {
|
||||
SearchReplaceToggleAction(this).createCustomComponent(ActionPlaces.UNKNOWN).apply {
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else null
|
||||
searchReplaceToggleComponent = searchReplaceToggle
|
||||
|
||||
val pnl = panel {
|
||||
|
|
@ -468,9 +476,11 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
cell(thinkingPanel).gap(RightGap.SMALL)
|
||||
cell(acceptChip).gap(RightGap.SMALL)
|
||||
cell(rejectChip).gap(RightGap.SMALL)
|
||||
if (showModeSelector) {
|
||||
cell(createToolbarSeparator()).gap(RightGap.SMALL)
|
||||
cell(searchReplaceToggle!!)
|
||||
cell(createToolbarSeparator()).gap(RightGap.SMALL)
|
||||
if (featureType == FeatureType.INLINE_EDIT) {
|
||||
cell(quickQuestionCheckbox)
|
||||
} else if (searchReplaceToggle != null) {
|
||||
cell(searchReplaceToggle)
|
||||
}
|
||||
}
|
||||
}.align(AlignX.LEFT)
|
||||
|
|
@ -478,6 +488,7 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
{
|
||||
panel {
|
||||
row {
|
||||
if (applyChip != null) cell(applyChip).gap(RightGap.SMALL)
|
||||
cell(submitButton).gap(RightGap.SMALL)
|
||||
cell(stopButton)
|
||||
}
|
||||
|
|
@ -493,7 +504,15 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
repaint()
|
||||
}
|
||||
|
||||
fun setThinkingVisible(visible: Boolean, text: String = "Thinking…") {
|
||||
fun setApplyVisible(visible: Boolean) {
|
||||
userInputHeaderPanel.setApplyVisible(visible)
|
||||
}
|
||||
|
||||
fun setApplyEnabled(enabled: Boolean) {
|
||||
userInputHeaderPanel.setApplyEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setThinkingVisible(visible: Boolean, text: String = CodeGPTBundle.get("shared.thinking")) {
|
||||
thinkingLabel.text = text
|
||||
thinkingIcon.isVisible = visible
|
||||
thinkingLabel.isVisible = visible
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import ee.carlrobert.codegpt.EncodingManager
|
|||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.ui.WrapLayout
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import ee.carlrobert.codegpt.ui.components.BadgeChip
|
||||
import ee.carlrobert.codegpt.ui.components.InlineEditChips
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import ee.carlrobert.codegpt.ui.textarea.TagDetailsComparator
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
|
|
@ -32,6 +38,8 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.Toolkit
|
||||
import java.awt.event.ActionListener
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JPanel
|
||||
|
|
@ -42,14 +50,16 @@ class UserInputHeaderPanel(
|
|||
private val tagManager: TagManager,
|
||||
private val totalTokensPanel: TotalTokensPanel,
|
||||
private val promptTextField: PromptTextField,
|
||||
private val withRemovableSelectedEditorTag: Boolean
|
||||
private val withRemovableSelectedEditorTag: Boolean,
|
||||
private val onApply: (() -> Unit)? = null,
|
||||
private val getMarkdownContent: (() -> String)? = null
|
||||
) : JPanel(WrapLayout(FlowLayout.LEFT, 4, 4)), TagManagerListener {
|
||||
|
||||
companion object {
|
||||
private const val INITIAL_VISIBLE_FILES = 2
|
||||
}
|
||||
|
||||
private val emptyText = JBLabel("No context included").apply {
|
||||
private val emptyText = JBLabel(CodeGPTBundle.get("userInput.noContextIncluded")).apply {
|
||||
foreground = JBUI.CurrentTheme.Label.disabledForeground()
|
||||
font = JBUI.Fonts.smallFont()
|
||||
isVisible = getSelectedEditor(project) == null
|
||||
|
|
@ -74,6 +84,34 @@ class UserInputHeaderPanel(
|
|||
add(emptyText)
|
||||
}
|
||||
|
||||
private val applyChip = onApply?.let { handler ->
|
||||
BadgeChip(CodeGPTBundle.get("shared.apply"), InlineEditChips.GREEN, handler)
|
||||
.apply { isVisible = false }
|
||||
}
|
||||
|
||||
private val copyButton = IconActionButton(
|
||||
object : AnAction(
|
||||
CodeGPTBundle.get("shared.copy"),
|
||||
CodeGPTBundle.get("shared.copyToClipboard"),
|
||||
AllIcons.Actions.Copy
|
||||
) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val text = getMarkdownContent?.invoke().orEmpty()
|
||||
if (text.isNotEmpty()) {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
clipboard.setContents(StringSelection(text), null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabled = !getMarkdownContent?.invoke().isNullOrEmpty()
|
||||
}
|
||||
},
|
||||
"COPY_MD"
|
||||
).apply { isVisible = getMarkdownContent != null }
|
||||
|
||||
private val backgroundScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
init {
|
||||
|
|
@ -233,9 +271,23 @@ class UserInputHeaderPanel(
|
|||
border = JBUI.Borders.empty()
|
||||
|
||||
add(defaultHeaderTagsPanel)
|
||||
applyChip?.let { add(it) }
|
||||
add(copyButton)
|
||||
addInitialTags()
|
||||
}
|
||||
|
||||
fun setApplyVisible(visible: Boolean) {
|
||||
applyChip?.isVisible = visible
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun setApplyEnabled(enabled: Boolean) {
|
||||
applyChip?.isEnabled = enabled
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun addInitialTags() {
|
||||
val autoTaggingEnabled =
|
||||
ConfigurationSettings.getState().chatCompletionSettings.editorContextTagEnabled
|
||||
|
|
@ -291,7 +343,7 @@ class UserInputHeaderPanel(
|
|||
isContentAreaFilled = false
|
||||
isOpaque = false
|
||||
border = null
|
||||
toolTipText = "Add Context"
|
||||
toolTipText = CodeGPTBundle.get("userInput.addContextTooltip")
|
||||
icon = IconUtil.scale(AllIcons.General.InlineAdd, null, 0.75f)
|
||||
rolloverIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
|
||||
pressedIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f)
|
||||
|
|
|
|||
|
|
@ -45,4 +45,22 @@ object MarkdownUtil {
|
|||
.build()
|
||||
.render(document)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract raw contents of fenced triple-backtick code blocks (without the fences or language).
|
||||
* Returns only non-blank code block bodies in order of appearance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun extractCodeBlocks(inputMarkdown: String): List<String> {
|
||||
val pattern = Pattern.compile(
|
||||
"(?ms)```([a-zA-Z0-9_+\-]*)\s*\r?\n([\s\S]*?)\r?\n```"
|
||||
)
|
||||
val matcher = pattern.matcher(inputMarkdown)
|
||||
val results = mutableListOf<String>()
|
||||
while (matcher.find()) {
|
||||
val content = matcher.group(2)
|
||||
if (!content.isNullOrBlank()) results.add(content)
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ shared.copyCode=Copy Code
|
|||
shared.copyMessageContents=Copy Message Contents
|
||||
shared.copyToClipboard=Copy to clipboard
|
||||
shared.copiedToClipboard=Copied to clipboard
|
||||
shared.thinking=Thinking…
|
||||
shared.configuration=Configuration
|
||||
shared.delete=Delete Message
|
||||
shared.deleteDescription=Delete message
|
||||
|
|
@ -436,3 +437,31 @@ inlineEdit.hint.searchingFor=Searching for: {0}
|
|||
inlineEdit.status.waiting=Waiting for model response…
|
||||
inlineEdit.status.noChanges=No applicable changes found
|
||||
inlineEdit.action.openInChat=Open in Chat
|
||||
shared.apply="Apply"
|
||||
|
||||
# Inline Edit / Ask related
|
||||
inlineEdit.conversation.title=Inline Edit ({0})
|
||||
inlineEdit.conversation.untitled=untitled
|
||||
inlineEdit.closeWarning.message=You have pending changes that will be lost. Do you want to close anyway?
|
||||
inlineEdit.closeWarning.title=Pending Changes
|
||||
inlineEdit.closeWarning.closeAnyway=Close Anyway
|
||||
inlineEdit.applying=Applying…
|
||||
inlineEdit.undo.acceptAll.commandTitle=Accept All Inline Edit Changes
|
||||
inlineEdit.undo.commandGroup=InlineEdit
|
||||
inlineEdit.apply.description=Apply changes to the editor
|
||||
inlineEdit.apply.changesTo=Apply changes to {0}
|
||||
inlineEdit.apply.noChanges=No changes to apply
|
||||
inlineEdit.apply.toFile=Apply to {0}
|
||||
|
||||
# Errors
|
||||
error.askRequestFailed=Ask mode request failed
|
||||
error.inlineEditRequestFailed=Inline Edit request failed
|
||||
|
||||
# User input panel
|
||||
userInput.quickQuestion=Quick question
|
||||
userInput.noContextIncluded=No context included
|
||||
userInput.addContextTooltip=Add Context
|
||||
|
||||
# Thought process panel
|
||||
thoughtProcess.thinking=Thinking...
|
||||
thoughtProcess.title=Thought Process
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue