feat: inline edit quick question

This commit is contained in:
Carl-Robert Linnupuu 2025-11-12 11:58:30 +00:00
parent 3a5ca4edf8
commit e7e9a2d9d8
28 changed files with 1029 additions and 391 deletions

View file

@ -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);

View file

@ -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();

View file

@ -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):
- 35 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 13 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 {
""
}
}
}
}

View file

@ -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))

View file

@ -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)
}
}
}

View file

@ -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() }
}
}

View file

@ -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() {

View file

@ -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")

View file

@ -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

View file

@ -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)
}

View file

@ -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
)

View file

@ -0,0 +1,6 @@
package ee.carlrobert.codegpt.inlineedit.engine
interface ApplyStrategy {
fun apply(ctx: ApplyContext)
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}

View file

@ -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() {

View file

@ -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
}

View file

@ -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,

View file

@ -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)
)
}

View file

@ -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
}
}
}
}

View file

@ -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()
}
}
}
})
}
}

View file

@ -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>"
}
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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
}
}

View file

@ -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