mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-13 15:32:25 +00:00
feat: replace the underlying input component with EditorTextField (#665)
This commit is contained in:
parent
a87ea8ade3
commit
6a3e894dda
15 changed files with 482 additions and 392 deletions
|
|
@ -29,15 +29,16 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails;
|
|||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel;
|
||||
import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel;
|
||||
import ee.carlrobert.codegpt.ui.OverlayUtil;
|
||||
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.WebSearchActionItem;
|
||||
import ee.carlrobert.codegpt.util.EditorUtil;
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JPanel;
|
||||
|
|
@ -52,7 +53,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
private final Project project;
|
||||
private final JPanel rootPanel;
|
||||
private final Conversation conversation;
|
||||
private final UserInputPanel textArea;
|
||||
private final UserInputPanel userInputPanel;
|
||||
private final ConversationService conversationService;
|
||||
private final TotalTokensPanel totalTokensPanel;
|
||||
private final ChatToolWindowScrollablePanel toolWindowScrollablePanel;
|
||||
|
|
@ -69,8 +70,12 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
conversation,
|
||||
EditorUtil.getSelectedEditorSelectedText(project),
|
||||
this);
|
||||
textArea = new UserInputPanel(project, this::handleSubmit, this::handleCancel);
|
||||
textArea.requestFocus();
|
||||
userInputPanel = new UserInputPanel(
|
||||
project,
|
||||
totalTokensPanel,
|
||||
this::handleSubmit,
|
||||
this::handleCancel);
|
||||
userInputPanel.requestFocus();
|
||||
rootPanel = createRootPanel();
|
||||
|
||||
if (conversation.getMessages().isEmpty()) {
|
||||
|
|
@ -97,7 +102,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
public void requestFocusForTextArea() {
|
||||
textArea.requestFocus();
|
||||
userInputPanel.requestFocus();
|
||||
}
|
||||
|
||||
public void displayLandingView() {
|
||||
|
|
@ -227,18 +232,18 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
conversationService,
|
||||
responsePanel,
|
||||
totalTokensPanel,
|
||||
textArea) {
|
||||
userInputPanel) {
|
||||
@Override
|
||||
public void handleTokensExceededPolicyAccepted() {
|
||||
call(callParameters, responsePanel);
|
||||
}
|
||||
});
|
||||
textArea.setSubmitEnabled(false);
|
||||
userInputPanel.setSubmitEnabled(false);
|
||||
|
||||
requestHandler.call(callParameters);
|
||||
}
|
||||
|
||||
private Unit handleSubmit(String text, boolean webSearchIncluded) {
|
||||
private Unit handleSubmit(String text, List<AppliedActionInlay> appliedInlayActions) {
|
||||
var message = new Message(text);
|
||||
var editor = EditorUtil.getSelectedEditor(project);
|
||||
if (editor != null) {
|
||||
|
|
@ -251,7 +256,8 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
}
|
||||
message.setUserMessage(text);
|
||||
message.setWebSearchIncluded(webSearchIncluded);
|
||||
message.setWebSearchIncluded(appliedInlayActions.stream()
|
||||
.anyMatch(it -> it.getSuggestion() instanceof WebSearchActionItem));
|
||||
|
||||
var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project);
|
||||
if (addedDocumentation != null) {
|
||||
|
|
@ -276,7 +282,7 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
JBUI.Borders.empty(8)));
|
||||
panel.add(JBUI.Panels.simplePanel(totalTokensPanel)
|
||||
.withBorder(JBUI.Borders.emptyBottom(8)), BorderLayout.NORTH);
|
||||
panel.add(JBUI.Panels.simplePanel(textArea), BorderLayout.CENTER);
|
||||
panel.add(JBUI.Panels.simplePanel(userInputPanel), BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
|
|
@ -326,20 +332,10 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
private JPanel createRootPanel() {
|
||||
var gbc = new GridBagConstraints();
|
||||
gbc.fill = GridBagConstraints.BOTH;
|
||||
gbc.weighty = 1;
|
||||
gbc.weightx = 1;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
|
||||
var rootPanel = new JPanel(new GridBagLayout());
|
||||
rootPanel.add(createScrollPaneWithSmartScroller(toolWindowScrollablePanel), gbc);
|
||||
|
||||
gbc.weighty = 0;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.gridy = 1;
|
||||
rootPanel.add(createUserPromptPanel(), gbc);
|
||||
var rootPanel = new JPanel(new BorderLayout());
|
||||
rootPanel.add(createScrollPaneWithSmartScroller(toolWindowScrollablePanel),
|
||||
BorderLayout.CENTER);
|
||||
rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH);
|
||||
return rootPanel;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.colors.EditorFontType
|
||||
import com.intellij.openapi.editor.ex.util.EditorUtil
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.event.ActionEvent
|
||||
import javax.swing.AbstractAction
|
||||
import javax.swing.JTextPane
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.text.DefaultStyledDocument
|
||||
import javax.swing.text.StyleConstants
|
||||
import javax.swing.text.StyleContext
|
||||
|
||||
class CustomTextPane(
|
||||
private val highlightedTextRanges: MutableList<Pair<TextRange, Boolean>>,
|
||||
private val onSubmit: (String) -> Unit
|
||||
) : JTextPane() {
|
||||
|
||||
companion object {
|
||||
private val logger = thisLogger()
|
||||
}
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
background = JBColor.namedColor("Editor.SearchField.background")
|
||||
document = DefaultStyledDocument()
|
||||
border = JBUI.Borders.empty(8)
|
||||
isFocusable = true
|
||||
font = if (Registry.`is`("ide.find.use.editor.font", false)) {
|
||||
EditorUtil.getEditorFont()
|
||||
} else {
|
||||
UIManager.getFont("TextField.font")
|
||||
}
|
||||
inputMap.put(KeyStroke.getKeyStroke("shift ENTER"), "insert-break")
|
||||
inputMap.put(KeyStroke.getKeyStroke("ENTER"), "text-submit")
|
||||
actionMap.put("text-submit", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
onSubmit(removeHighlightedText(text))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun appendHighlightedText(
|
||||
text: String,
|
||||
searchChar: Char = '@',
|
||||
withWhitespace: Boolean = true,
|
||||
replacement: Boolean = true
|
||||
): TextRange? {
|
||||
val lastIndex = this.text.lastIndexOf(searchChar)
|
||||
if (lastIndex != -1) {
|
||||
val styleContext = StyleContext.getDefaultStyleContext()
|
||||
val fileNameStyle = styleContext.addStyle("smart-highlighter", null)
|
||||
val fontFamily = service<EditorColorsManager>().globalScheme
|
||||
.getFont(EditorFontType.PLAIN)
|
||||
.deriveFont(JBFont.label().size.toFloat())
|
||||
.family
|
||||
|
||||
StyleConstants.setFontFamily(fileNameStyle, fontFamily)
|
||||
StyleConstants.setForeground(
|
||||
fileNameStyle,
|
||||
JBUI.CurrentTheme.GotItTooltip.codeForeground(true)
|
||||
)
|
||||
StyleConstants.setBackground(
|
||||
fileNameStyle,
|
||||
JBUI.CurrentTheme.GotItTooltip.codeBackground(true)
|
||||
)
|
||||
|
||||
val startOffset = lastIndex + 1
|
||||
document.remove(startOffset, document.length - startOffset)
|
||||
document.insertString(startOffset, text, fileNameStyle)
|
||||
styledDocument.setCharacterAttributes(
|
||||
lastIndex,
|
||||
text.length,
|
||||
fileNameStyle,
|
||||
true
|
||||
)
|
||||
if (withWhitespace) {
|
||||
document.insertString(
|
||||
document.length,
|
||||
" ",
|
||||
styleContext.getStyle(StyleContext.DEFAULT_STYLE)
|
||||
)
|
||||
}
|
||||
val modifiedStartOffset = if (searchChar == '@') startOffset - 1 else startOffset
|
||||
val endOffset = startOffset + text.length + (if (withWhitespace) 1 else 0)
|
||||
val textRange = TextRange(modifiedStartOffset, endOffset)
|
||||
highlightedTextRanges.add(Pair(textRange, replacement))
|
||||
return textRange
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
super.paintComponent(g)
|
||||
val g2d = g as Graphics2D
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
if (document.length == 0) {
|
||||
g2d.color = JBColor.GRAY
|
||||
g2d.font = if (Registry.`is`("ide.find.use.editor.font", false)) {
|
||||
EditorUtil.getEditorFont()
|
||||
} else {
|
||||
UIManager.getFont("TextField.font")
|
||||
}
|
||||
g2d.drawString(
|
||||
CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"),
|
||||
insets.left,
|
||||
g2d.fontMetrics.maxAscent + insets.top
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeHighlightedText(text: String): String {
|
||||
try {
|
||||
var result = text
|
||||
highlightedTextRanges.forEach { (textRange, replacement) ->
|
||||
if (replacement) {
|
||||
result = result.replace(
|
||||
text.substring(textRange.startOffset, textRange.endOffset),
|
||||
""
|
||||
)
|
||||
}
|
||||
}
|
||||
return result.trim()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error while removing highlighted text", e)
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.jetbrains.rd.util.AtomicReference
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.text.StyleContext
|
||||
import javax.swing.text.StyledDocument
|
||||
|
||||
class CustomTextPaneKeyAdapter(
|
||||
private val project: Project,
|
||||
private val textPane: CustomTextPane,
|
||||
private val highlightedTextRanges: MutableList<Pair<TextRange, Boolean>>,
|
||||
onWebSearchIncluded: () -> Unit
|
||||
) : KeyAdapter() {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private val suggestionsPopupManager =
|
||||
SuggestionsPopupManager(project, textPane, onWebSearchIncluded)
|
||||
private val popupOpenedAtRange: AtomicReference<TextRange?> = AtomicReference(null)
|
||||
|
||||
override fun keyReleased(e: KeyEvent) {
|
||||
if (textPane.text.isEmpty()) {
|
||||
// TODO: Remove only the files that were added via shortcuts
|
||||
project.service<FileSearchService>().removeFilesFromSession()
|
||||
project.putUserData(CodeGPTKeys.ADDED_DOCUMENTATION, null)
|
||||
highlightedTextRanges.clear()
|
||||
suggestionsPopupManager.hidePopup()
|
||||
return
|
||||
}
|
||||
if (e.keyCode == KeyEvent.VK_BACK_SPACE) {
|
||||
if (popupOpenedAtRange.get() == TextRange(
|
||||
textPane.caretPosition,
|
||||
textPane.caretPosition + 1
|
||||
)
|
||||
) {
|
||||
suggestionsPopupManager.hidePopup()
|
||||
return
|
||||
}
|
||||
|
||||
if (textPane.text.isNotEmpty() && textPane.text.last() == '@') {
|
||||
suggestionsPopupManager.reset()
|
||||
}
|
||||
}
|
||||
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_UP, KeyEvent.VK_DOWN -> {
|
||||
suggestionsPopupManager.requestFocus()
|
||||
suggestionsPopupManager.selectNext()
|
||||
e.consume()
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (suggestionsPopupManager.isPopupVisible()) {
|
||||
updateSuggestions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun keyTyped(e: KeyEvent) {
|
||||
val popupVisible = suggestionsPopupManager.isPopupVisible()
|
||||
if (e.keyChar == '@' && !popupVisible) {
|
||||
suggestionsPopupManager.showPopup(textPane)
|
||||
popupOpenedAtRange.getAndSet(
|
||||
TextRange(
|
||||
textPane.caretPosition,
|
||||
textPane.caretPosition + 1
|
||||
)
|
||||
)
|
||||
return
|
||||
} else if (popupVisible) {
|
||||
updateSuggestions()
|
||||
}
|
||||
|
||||
val doc = textPane.document as StyledDocument
|
||||
if (textPane.caretPosition >= 0) {
|
||||
doc.setCharacterAttributes(
|
||||
textPane.caretPosition,
|
||||
1,
|
||||
StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.keyChar == '\t') {
|
||||
suggestionsPopupManager.requestFocus()
|
||||
suggestionsPopupManager.selectNext()
|
||||
e.consume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSuggestions() {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
val text = textPane.text
|
||||
val lastAtIndex = text.lastIndexOf('@')
|
||||
if (lastAtIndex != -1) {
|
||||
val lastAtSearchIndex = text.lastIndexOf(':')
|
||||
if (lastAtSearchIndex != -1) {
|
||||
val searchText = text.substring(lastAtSearchIndex + 1)
|
||||
if (searchText.isNotEmpty()) {
|
||||
launch {
|
||||
suggestionsPopupManager.updateSuggestions(searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
suggestionsPopupManager.hidePopup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.ide.IdeEventQueue
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.application.runUndoTransparentWriteAction
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.Inlay
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.fileTypes.FileTypes
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.ComponentUtil.findParentByCondition
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.CodeGPTKeys
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem
|
||||
import java.awt.AWTEvent
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
|
||||
data class AppliedActionInlay(
|
||||
val suggestion: SuggestionItem,
|
||||
val inlay: Inlay<PromptTextFieldInlayRenderer?>,
|
||||
)
|
||||
|
||||
class PromptTextField(
|
||||
project: Project,
|
||||
onTextChanged: (String) -> Unit,
|
||||
private val onSubmit: (String, List<AppliedActionInlay>) -> Unit
|
||||
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
|
||||
|
||||
val dispatcherId: UUID = UUID.randomUUID()
|
||||
|
||||
private val appliedInlays: MutableList<AppliedActionInlay> = mutableListOf()
|
||||
private val suggestionsPopupManager = SuggestionsPopupManager(project, this)
|
||||
|
||||
init {
|
||||
isOneLineMode = false
|
||||
minimumSize = Dimension(100, 40)
|
||||
background = Color(0, 0, 0, 0)
|
||||
document.addDocumentListener(getDocumentListener(onTextChanged))
|
||||
setPlaceholder(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"))
|
||||
IdeEventQueue.getInstance().addDispatcher(
|
||||
PromptTextFieldEventDispatcher(this, suggestionsPopupManager, appliedInlays) {
|
||||
onSubmit(text, appliedInlays)
|
||||
clear()
|
||||
},
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
fun addInlineText(actionPrefix: String, text: String?, actionItem: SuggestionActionItem) {
|
||||
editor?.let {
|
||||
val startOffset = it.caretModel.offset - 1
|
||||
|
||||
runUndoTransparentWriteAction {
|
||||
it.document.deleteString(startOffset, it.document.textLength)
|
||||
it.document.setText(it.document.text + " ")
|
||||
appliedInlays.add(
|
||||
AppliedActionInlay(
|
||||
actionItem,
|
||||
it.inlayModel.addInlineElement(
|
||||
startOffset,
|
||||
true,
|
||||
PromptTextFieldInlayRenderer(actionPrefix, text) { inlay ->
|
||||
inlay.dispose()
|
||||
})!!,
|
||||
)
|
||||
)
|
||||
it.caretModel.moveToOffset(it.document.textLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createEditor(): EditorEx {
|
||||
val editorEx = super.createEditor()
|
||||
editorEx.settings.isUseSoftWraps = true
|
||||
return editorEx
|
||||
}
|
||||
|
||||
override fun updateBorder(editor: EditorEx) {
|
||||
editor.setBorder(JBUI.Borders.empty(4, 8))
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
clear()
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
runInEdt { text = "" }
|
||||
clearInlays()
|
||||
}
|
||||
|
||||
private fun clearInlays() {
|
||||
runUndoTransparentWriteAction {
|
||||
appliedInlays.forEach { it.inlay.dispose() }
|
||||
appliedInlays.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDocumentListener(onTextChanged: (String) -> Unit): DocumentListener {
|
||||
return object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
onTextChanged(event.document.text)
|
||||
|
||||
if (event.document.text.isEmpty()) {
|
||||
project.service<FileSearchService>().removeFilesFromSession()
|
||||
project.putUserData(CodeGPTKeys.ADDED_DOCUMENTATION, null)
|
||||
suggestionsPopupManager.hidePopup()
|
||||
clearInlays()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PromptTextFieldEventDispatcher(
|
||||
private val textField: PromptTextField,
|
||||
private val suggestionsPopupManager: SuggestionsPopupManager,
|
||||
private val appliedInlays: MutableList<AppliedActionInlay>,
|
||||
private val onSubmit: () -> Unit
|
||||
) : IdeEventQueue.EventDispatcher {
|
||||
|
||||
companion object {
|
||||
const val AT_CHAR = '@'
|
||||
}
|
||||
|
||||
override fun dispatch(e: AWTEvent): Boolean {
|
||||
val owner =
|
||||
findParentByCondition(KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner) { component ->
|
||||
component is PromptTextField
|
||||
}
|
||||
|
||||
if ((e is KeyEvent || e is MouseEvent)
|
||||
&& owner is PromptTextField
|
||||
&& owner.dispatcherId == textField.dispatcherId
|
||||
) {
|
||||
owner.revalidate()
|
||||
owner.repaint()
|
||||
|
||||
if (e is KeyEvent) {
|
||||
if (e.id == KeyEvent.KEY_PRESSED) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_BACK_SPACE -> {
|
||||
if (textField.text.let { it.isNotEmpty() && it.last() == AT_CHAR }) {
|
||||
suggestionsPopupManager.reset()
|
||||
}
|
||||
|
||||
val appliedInlay = appliedInlays.find {
|
||||
it.inlay.offset == owner.caretModel.offset - 1
|
||||
}
|
||||
if (appliedInlay != null) {
|
||||
appliedInlay.inlay.dispose()
|
||||
appliedInlays.remove(appliedInlay)
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.VK_TAB -> {
|
||||
selectNextSuggestion(e)
|
||||
}
|
||||
|
||||
KeyEvent.VK_ENTER -> {
|
||||
if (e.modifiersEx and InputEvent.SHIFT_DOWN_MASK == 0
|
||||
&& e.modifiersEx and InputEvent.ALT_DOWN_MASK == 0
|
||||
&& e.modifiersEx and InputEvent.CTRL_DOWN_MASK == 0
|
||||
) {
|
||||
onSubmit()
|
||||
e.consume()
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.VK_UP -> selectPreviousSuggestion(e)
|
||||
KeyEvent.VK_DOWN -> selectNextSuggestion(e)
|
||||
}
|
||||
when (e.keyChar) {
|
||||
AT_CHAR -> showPopup(e)
|
||||
else -> {
|
||||
if (suggestionsPopupManager.isPopupVisible()) {
|
||||
updateSuggestions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return e.isConsumed
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun selectNextSuggestion(event: KeyEvent) {
|
||||
suggestionsPopupManager.selectNext()
|
||||
event.consume()
|
||||
}
|
||||
|
||||
private fun selectPreviousSuggestion(event: KeyEvent) {
|
||||
suggestionsPopupManager.selectPrevious()
|
||||
event.consume()
|
||||
}
|
||||
|
||||
private fun showPopup(event: KeyEvent) {
|
||||
suggestionsPopupManager.showPopup()
|
||||
event.consume()
|
||||
}
|
||||
|
||||
private fun updateSuggestions() {
|
||||
val lastAtIndex = textField.text.lastIndexOf(AT_CHAR)
|
||||
if (lastAtIndex != -1) {
|
||||
val searchText = textField.text.substring(lastAtIndex + 1)
|
||||
if (searchText.isNotEmpty()) {
|
||||
suggestionsPopupManager.updateSuggestions(searchText)
|
||||
}
|
||||
} else {
|
||||
suggestionsPopupManager.hidePopup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
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.ui.JBColor
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.Cursor
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.geom.Rectangle2D
|
||||
|
||||
class PromptTextFieldInlayRenderer(
|
||||
private val actionPrefix: String,
|
||||
private val text: String?,
|
||||
private val onClose: (Inlay<*>) -> Unit
|
||||
) : EditorCustomElementRenderer {
|
||||
|
||||
private val closeIcon = AllIcons.Actions.Close
|
||||
|
||||
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 ""))
|
||||
return textWidth + closeIcon.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)
|
||||
|
||||
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 = service<EditorColorsManager>().globalScheme.defaultForeground
|
||||
g.drawString(actionPrefix, startX, startY)
|
||||
|
||||
if (!text.isNullOrEmpty()) {
|
||||
val prefixWidth = metrics.stringWidth(actionPrefix)
|
||||
g.color = textAttributes?.foregroundColor ?: JBColor.foreground()
|
||||
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 addMouseListeners(editor: Editor, inlay: Inlay<*>, target: Rectangle2D) {
|
||||
fun isWithinIconBounds(e: MouseEvent): Boolean {
|
||||
val iconX = (target.x + target.width - closeIcon.iconWidth - JBUI.scale(5)).toInt()
|
||||
val iconY = (target.y + (target.height - closeIcon.iconHeight) / 2).toInt()
|
||||
return e.x >= iconX && e.x <= iconX + closeIcon.iconWidth &&
|
||||
e.y >= iconY && e.y <= iconY + closeIcon.iconHeight
|
||||
}
|
||||
|
||||
fun updateCursor(event: MouseEvent, inlay: Inlay<*>) {
|
||||
editor.contentComponent.let {
|
||||
if (inlay.isValid) {
|
||||
val inlayBounds = inlay.bounds
|
||||
if (inlayBounds != null && inlayBounds.contains(event.x, event.y)) {
|
||||
it.cursor = if (isWithinIconBounds(event)) {
|
||||
Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
} else {
|
||||
Cursor.getDefaultCursor()
|
||||
}
|
||||
return@let
|
||||
}
|
||||
}
|
||||
it.cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)
|
||||
}
|
||||
}
|
||||
|
||||
editor.addEditorMouseMotionListener(object : EditorMouseMotionListener {
|
||||
override fun mouseMoved(event: EditorMouseEvent) {
|
||||
updateCursor(event.mouseEvent, inlay)
|
||||
}
|
||||
})
|
||||
|
||||
editor.addEditorMouseListener(object : EditorMouseListener {
|
||||
override fun mouseClicked(event: EditorMouseEvent) {
|
||||
if (isWithinIconBounds(event.mouseEvent)) {
|
||||
onClose(inlay)
|
||||
event.consume()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent
|
|||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.ui.components.AnActionLink
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
|
|
@ -21,25 +20,22 @@ import ee.carlrobert.codegpt.conversations.ConversationsState
|
|||
import ee.carlrobert.codegpt.settings.GeneralSettings
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
|
||||
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 java.awt.*
|
||||
import javax.swing.JPanel
|
||||
|
||||
class UserInputPanel(
|
||||
private val project: Project,
|
||||
private val onSubmit: (String, Boolean) -> Unit,
|
||||
private val totalTokensPanel: TotalTokensPanel,
|
||||
private val onSubmit: (String, List<AppliedActionInlay>?) -> Unit,
|
||||
private val onStop: () -> Unit
|
||||
) : JPanel(BorderLayout()) {
|
||||
|
||||
private val highlightedTextRanges: MutableList<Pair<TextRange, Boolean>> = mutableListOf()
|
||||
|
||||
private val textPane = CustomTextPane(highlightedTextRanges) { handleSubmit(it) }
|
||||
.apply {
|
||||
addKeyListener(CustomTextPaneKeyAdapter(project, this, highlightedTextRanges) {
|
||||
webSearchIncluded = true
|
||||
})
|
||||
}
|
||||
val text: String
|
||||
get() = promptTextField.text
|
||||
|
||||
private val promptTextField = PromptTextField(project, ::updateUserTokens, ::handleSubmit)
|
||||
private val submitButton = IconActionButton(
|
||||
object : AnAction(
|
||||
CodeGPTBundle.get("smartTextPane.submitButton.title"),
|
||||
|
|
@ -47,7 +43,7 @@ class UserInputPanel(
|
|||
Icons.Send
|
||||
) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
handleSubmit(textPane.text)
|
||||
handleSubmit(promptTextField.text)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -63,14 +59,10 @@ class UserInputPanel(
|
|||
}
|
||||
).apply { isEnabled = false }
|
||||
private val imageActionSupported = AtomicBooleanProperty(isImageActionSupported())
|
||||
private var webSearchIncluded: Boolean = false
|
||||
|
||||
val text: String
|
||||
get() = textPane.text
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
add(textPane, BorderLayout.CENTER)
|
||||
add(promptTextField, BorderLayout.CENTER)
|
||||
add(getFooter(), BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
|
|
@ -80,8 +72,8 @@ class UserInputPanel(
|
|||
}
|
||||
|
||||
override fun requestFocus() {
|
||||
textPane.requestFocus()
|
||||
textPane.requestFocusInWindow()
|
||||
promptTextField.requestFocus()
|
||||
promptTextField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
|
|
@ -97,7 +89,7 @@ class UserInputPanel(
|
|||
val g2 = g.create() as Graphics2D
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.color = JBUI.CurrentTheme.ActionButton.focusedBorder()
|
||||
if (textPane.isFocusOwner) {
|
||||
if (promptTextField.isFocusOwner) {
|
||||
g2.stroke = BasicStroke(1.5F)
|
||||
}
|
||||
g2.drawRoundRect(0, 0, width - 1, height - 1, 16, 16)
|
||||
|
|
@ -106,14 +98,16 @@ class UserInputPanel(
|
|||
|
||||
override fun getInsets(): Insets = JBUI.insets(4)
|
||||
|
||||
private fun handleSubmit(text: String) {
|
||||
private fun handleSubmit(text: String, appliedInlays: List<AppliedActionInlay>? = emptyList()) {
|
||||
if (text.isNotEmpty()) {
|
||||
onSubmit(text, webSearchIncluded)
|
||||
highlightedTextRanges.clear()
|
||||
textPane.text = ""
|
||||
onSubmit(text, appliedInlays)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserTokens(text: String) {
|
||||
totalTokensPanel.updateUserPromptTokens(text)
|
||||
}
|
||||
|
||||
private fun getFooter(): JPanel {
|
||||
val attachImageLink = AnActionLink(CodeGPTBundle.get("shared.image"), AttachImageAction())
|
||||
.apply {
|
||||
|
|
@ -151,4 +145,4 @@ class UserInputPanel(
|
|||
private fun isImageActionSupported(): Boolean {
|
||||
return service<GeneralSettings>().state.selectedService.isImageActionSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion
|
|||
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionListCellRenderer
|
||||
import java.awt.KeyboardFocusManager
|
||||
|
|
@ -15,7 +15,7 @@ import javax.swing.ListSelectionModel
|
|||
|
||||
class SuggestionList(
|
||||
listModel: DefaultListModel<SuggestionItem>,
|
||||
private val textPane: CustomTextPane,
|
||||
private val textPane: PromptTextField,
|
||||
private val onSelected: (SuggestionItem) -> Unit
|
||||
) : JBList<SuggestionItem>(listModel) {
|
||||
|
||||
|
|
@ -27,6 +27,19 @@ class SuggestionList(
|
|||
setupMouseMotionListener()
|
||||
}
|
||||
|
||||
fun selectNext() {
|
||||
updateSelectedIndex(if (selectedIndex < model.size - 1) selectedIndex + 1 else 0)
|
||||
}
|
||||
|
||||
fun selectPrevious() {
|
||||
updateSelectedIndex(if (selectedIndex > 0) selectedIndex - 1 else model.size - 1)
|
||||
}
|
||||
|
||||
private fun updateSelectedIndex(newIndex: Int) {
|
||||
selectedIndex = newIndex
|
||||
ensureIndexIsVisible(newIndex)
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
border = JBUI.Borders.empty()
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
|
|
@ -101,10 +114,4 @@ class SuggestionList(
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectNext() {
|
||||
val newIndex = if (selectedIndex < model.size - 1) selectedIndex + 1 else 0
|
||||
selectedIndex = newIndex
|
||||
ensureIndexIsVisible(newIndex)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ class SuggestionsPopupBuilder {
|
|||
private var preferableFocusComponent: JComponent? = null
|
||||
private var onCancel: () -> Boolean = { true }
|
||||
|
||||
fun setPreferableFocusComponent(preferableFocusComponent: JComponent): SuggestionsPopupBuilder {
|
||||
fun setPreferableFocusComponent(preferableFocusComponent: JComponent?): SuggestionsPopupBuilder {
|
||||
this.preferableFocusComponent = preferableFocusComponent
|
||||
return this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion
|
|||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.popup.JBPopup
|
||||
import com.intellij.vcsUtil.showAbove
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Dimension
|
||||
|
|
@ -15,11 +15,9 @@ import javax.swing.event.ListDataListener
|
|||
|
||||
class SuggestionsPopupManager(
|
||||
private val project: Project,
|
||||
private val textPane: CustomTextPane,
|
||||
onWebSearchIncluded: () -> Unit,
|
||||
private val textField: PromptTextField,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
private var selectedActionGroup: SuggestionGroupItem? = null
|
||||
private var popup: JBPopup? = null
|
||||
private var originalLocation: Point? = null
|
||||
|
|
@ -30,7 +28,7 @@ class SuggestionsPopupManager(
|
|||
override fun contentsChanged(e: ListDataEvent) {}
|
||||
})
|
||||
}
|
||||
private val list = SuggestionList(listModel, textPane) {
|
||||
private val list = SuggestionList(listModel, textField) {
|
||||
handleSuggestionItemSelection(it)
|
||||
}
|
||||
private val defaultActions: MutableList<SuggestionItem> = mutableListOf(
|
||||
|
|
@ -38,10 +36,10 @@ class SuggestionsPopupManager(
|
|||
FolderSuggestionGroupItem(project),
|
||||
PersonaSuggestionGroupItem(),
|
||||
DocumentationSuggestionGroupItem(),
|
||||
WebSearchActionItem(onWebSearchIncluded),
|
||||
WebSearchActionItem(),
|
||||
)
|
||||
|
||||
fun showPopup(component: JComponent) {
|
||||
fun showPopup(component: JComponent? = null) {
|
||||
popup = SuggestionsPopupBuilder()
|
||||
.setPreferableFocusComponent(component)
|
||||
.setOnCancel {
|
||||
|
|
@ -49,11 +47,9 @@ class SuggestionsPopupManager(
|
|||
true
|
||||
}
|
||||
.build(list)
|
||||
popup?.showAbove(component)
|
||||
originalLocation = component.locationOnScreen
|
||||
popup?.showAbove(textField)
|
||||
originalLocation = textField.locationOnScreen
|
||||
reset(true)
|
||||
// TODO: Apply initial focus to the popup until a proper search mechanism is in place.
|
||||
requestFocus()
|
||||
selectNext()
|
||||
}
|
||||
|
||||
|
|
@ -65,24 +61,28 @@ class SuggestionsPopupManager(
|
|||
return popup?.isVisible ?: false
|
||||
}
|
||||
|
||||
fun requestFocus() {
|
||||
list.requestFocus()
|
||||
}
|
||||
|
||||
fun selectNext() {
|
||||
list.requestFocus()
|
||||
list.selectNext()
|
||||
}
|
||||
|
||||
suspend fun updateSuggestions(searchText: String? = null) {
|
||||
val suggestions = withContext(Dispatchers.Default) {
|
||||
selectedActionGroup?.getSuggestions(searchText) ?: emptyList()
|
||||
}
|
||||
fun selectPrevious() {
|
||||
list.requestFocus()
|
||||
list.selectPrevious()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
listModel.clear()
|
||||
listModel.addAll(suggestions)
|
||||
list.revalidate()
|
||||
list.repaint()
|
||||
fun updateSuggestions(searchText: String? = null) {
|
||||
scope.launch {
|
||||
val suggestions = withContext(Dispatchers.Default) {
|
||||
selectedActionGroup?.getSuggestions(searchText) ?: emptyList()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
listModel.clear()
|
||||
listModel.addAll(suggestions)
|
||||
list.revalidate()
|
||||
list.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,16 +99,13 @@ class SuggestionsPopupManager(
|
|||
when (item) {
|
||||
is SuggestionActionItem -> {
|
||||
hidePopup()
|
||||
item.execute(project, textPane)
|
||||
item.execute(project, textField)
|
||||
}
|
||||
|
||||
is SuggestionGroupItem -> {
|
||||
selectedActionGroup = item
|
||||
scope.launch {
|
||||
updateSuggestions()
|
||||
}
|
||||
textPane.appendHighlightedText(item.groupPrefix, withWhitespace = false)
|
||||
textPane.requestFocus()
|
||||
updateSuggestions()
|
||||
textField.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +118,6 @@ class SuggestionsPopupManager(
|
|||
list.repaint()
|
||||
|
||||
popup?.size = Dimension(list.preferredSize.width, list.preferredSize.height + 32)
|
||||
|
||||
originalLocation?.let { original ->
|
||||
val newY = original.y - list.preferredSize.height - 32
|
||||
popup?.setLocation(Point(original.x, maxOf(newY, 0)))
|
||||
|
|
|
|||
|
|
@ -16,16 +16,16 @@ import ee.carlrobert.codegpt.settings.persona.PersonasConfigurable
|
|||
import ee.carlrobert.codegpt.settings.service.ServiceType
|
||||
import ee.carlrobert.codegpt.ui.AddDocumentationDialog
|
||||
import ee.carlrobert.codegpt.ui.DocumentationDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.FileSearchService
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
|
||||
class FileActionItem(val file: VirtualFile) : SuggestionActionItem {
|
||||
override val displayName = file.name
|
||||
override val icon = file.fileType.icon ?: AllIcons.FileTypes.Any_type
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
project.getService(FileSearchService::class.java).addFileToSession(file)
|
||||
textPane.appendHighlightedText(file.name, ':', replacement = false)
|
||||
textPane.addInlineText("file", file.name, this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,12 +33,12 @@ class FolderActionItem(val folder: VirtualFile) : SuggestionActionItem {
|
|||
override val displayName = folder.name
|
||||
override val icon = AllIcons.Nodes.Folder
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
val fileSearchService = project.service<FileSearchService>()
|
||||
folder.children
|
||||
.filter { !it.isDirectory }
|
||||
.forEach { fileSearchService.addFileToSession(it) }
|
||||
textPane.appendHighlightedText(folder.path, ':', replacement = false)
|
||||
textPane.addInlineText("folder", folder.path, this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,13 +46,13 @@ class PersonaActionItem(val personaDetails: PersonaDetails) : SuggestionActionIt
|
|||
override val displayName = personaDetails.name
|
||||
override val icon = AllIcons.General.User
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
service<PersonaSettings>().state.selectedPersona.apply {
|
||||
id = personaDetails.id
|
||||
name = personaDetails.name
|
||||
instructions = personaDetails.instructions
|
||||
}
|
||||
textPane.appendHighlightedText(personaDetails.name, ':')
|
||||
textPane.addInlineText("persona", personaDetails.name, this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +63,10 @@ class DocumentationActionItem(
|
|||
override val icon = AllIcons.Toolwindows.Documentation
|
||||
override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
CodeGPTKeys.ADDED_DOCUMENTATION.set(project, documentationDetails)
|
||||
service<DocumentationSettings>().updateLastUsedDateTime(documentationDetails.url)
|
||||
textPane.appendHighlightedText(documentationDetails.name, ':')
|
||||
textPane.addInlineText("doc", documentationDetails.name, this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,14 +76,15 @@ class CreateDocumentationActionItem : SuggestionActionItem {
|
|||
override val icon = AllIcons.General.Add
|
||||
override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
val addDocumentationDialog = AddDocumentationDialog(project)
|
||||
if (addDocumentationDialog.showAndGet()) {
|
||||
service<DocumentationSettings>()
|
||||
.updateLastUsedDateTime(addDocumentationDialog.documentationDetails.url)
|
||||
textPane.appendHighlightedText(
|
||||
textPane.addInlineText(
|
||||
"doc",
|
||||
addDocumentationDialog.documentationDetails.name,
|
||||
searchChar = ':',
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ class ViewAllDocumentationsActionItem : SuggestionActionItem {
|
|||
override val icon = null
|
||||
override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
service<ShowSettingsUtil>().showSettingsDialog(
|
||||
project,
|
||||
DocumentationsConfigurable::class.java
|
||||
|
|
@ -108,7 +109,7 @@ class CreatePersonaActionItem : SuggestionActionItem {
|
|||
CodeGPTBundle.get("suggestionActionItem.createPersona.displayName")
|
||||
override val icon = AllIcons.General.Add
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
service<ShowSettingsUtil>().showSettingsDialog(
|
||||
project,
|
||||
PersonasConfigurable::class.java
|
||||
|
|
@ -116,14 +117,13 @@ class CreatePersonaActionItem : SuggestionActionItem {
|
|||
}
|
||||
}
|
||||
|
||||
class WebSearchActionItem(private val onWebSearchIncluded: () -> Unit) : SuggestionActionItem {
|
||||
class WebSearchActionItem : SuggestionActionItem {
|
||||
override val displayName: String =
|
||||
CodeGPTBundle.get("suggestionActionItem.webSearch.displayName")
|
||||
override val icon = AllIcons.General.Web
|
||||
override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT
|
||||
|
||||
override fun execute(project: Project, textPane: CustomTextPane) {
|
||||
onWebSearchIncluded()
|
||||
textPane.appendHighlightedText("web")
|
||||
override fun execute(project: Project, textPane: PromptTextField) {
|
||||
textPane.addInlineText("web", null, this)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ import java.time.format.DateTimeParseException
|
|||
|
||||
class FileSuggestionGroupItem(private val project: Project) : SuggestionGroupItem {
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.files.displayName")
|
||||
override val groupPrefix = "file:"
|
||||
override val icon = AllIcons.FileTypes.Any_type
|
||||
|
||||
override suspend fun getSuggestions(searchText: String?): List<SuggestionActionItem> {
|
||||
|
|
@ -43,7 +42,6 @@ class FolderSuggestionGroupItem(private val project: Project) : SuggestionGroupI
|
|||
private val projectFoldersCache = mutableMapOf<Project, List<VirtualFile>>()
|
||||
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.folders.displayName")
|
||||
override val groupPrefix = "folder:"
|
||||
override val icon = AllIcons.Nodes.Folder
|
||||
|
||||
override suspend fun getSuggestions(searchText: String?): List<SuggestionActionItem> {
|
||||
|
|
@ -78,7 +76,6 @@ class FolderSuggestionGroupItem(private val project: Project) : SuggestionGroupI
|
|||
|
||||
class PersonaSuggestionGroupItem : SuggestionGroupItem {
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.personas.displayName")
|
||||
override val groupPrefix = "persona:"
|
||||
override val icon = AllIcons.General.User
|
||||
|
||||
override suspend fun getSuggestions(searchText: String?): List<SuggestionActionItem> =
|
||||
|
|
@ -96,7 +93,6 @@ class PersonaSuggestionGroupItem : SuggestionGroupItem {
|
|||
|
||||
class DocumentationSuggestionGroupItem : SuggestionGroupItem {
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.docs.displayName")
|
||||
override val groupPrefix = "doc:"
|
||||
override val icon = AllIcons.Toolwindows.Documentation
|
||||
override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.suggestion.item
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import javax.swing.Icon
|
||||
|
||||
interface SuggestionItem {
|
||||
|
|
@ -12,11 +12,9 @@ interface SuggestionItem {
|
|||
}
|
||||
|
||||
interface SuggestionActionItem : SuggestionItem {
|
||||
fun execute(project: Project, textPane: CustomTextPane)
|
||||
fun execute(project: Project, textPane: PromptTextField)
|
||||
}
|
||||
|
||||
interface SuggestionGroupItem : SuggestionItem {
|
||||
val groupPrefix: String
|
||||
|
||||
suspend fun getSuggestions(searchText: String? = null): List<SuggestionItem>
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import com.intellij.ui.dsl.builder.panel
|
|||
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.settings.persona.PersonaSettings
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.*
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionItemRendererTextUtils.highlightSearchText
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionItemRendererTextUtils.searchText
|
||||
|
|
@ -24,7 +24,7 @@ interface ItemRenderer {
|
|||
fun render(component: JLabel, value: SuggestionItem): JPanel
|
||||
}
|
||||
|
||||
abstract class BaseItemRenderer(private val textPane: CustomTextPane) : ItemRenderer {
|
||||
abstract class BaseItemRenderer(private val textField: PromptTextField) : ItemRenderer {
|
||||
protected fun createPanel(
|
||||
label: JLabel,
|
||||
icon: Icon,
|
||||
|
|
@ -33,7 +33,7 @@ abstract class BaseItemRenderer(private val textPane: CustomTextPane) : ItemRend
|
|||
toolTipText: String?,
|
||||
truncateFromStart: Boolean = false
|
||||
): JPanel {
|
||||
val searchText = textPane.text.searchText()
|
||||
val searchText = textField.text.searchText()
|
||||
label.apply {
|
||||
this.icon = icon
|
||||
disabledIcon = icon
|
||||
|
|
@ -60,7 +60,7 @@ abstract class BaseItemRenderer(private val textPane: CustomTextPane) : ItemRend
|
|||
}
|
||||
}
|
||||
|
||||
class FileItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
||||
class FileItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) {
|
||||
override fun render(component: JLabel, value: SuggestionItem): JPanel {
|
||||
val item = value as FileActionItem
|
||||
val icon =
|
||||
|
|
@ -71,7 +71,7 @@ class FileItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
|||
}
|
||||
}
|
||||
|
||||
class FolderItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
||||
class FolderItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) {
|
||||
override fun render(component: JLabel, value: SuggestionItem): JPanel {
|
||||
val item = value as FolderActionItem
|
||||
return createPanel(
|
||||
|
|
@ -85,7 +85,7 @@ class FolderItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane)
|
|||
}
|
||||
}
|
||||
|
||||
class DefaultItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
||||
class DefaultItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) {
|
||||
companion object {
|
||||
private val EMPTY_ICON = ImageIcon(BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB))
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ class DefaultItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane)
|
|||
}
|
||||
}
|
||||
|
||||
class PersonaItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
||||
class PersonaItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) {
|
||||
override fun render(component: JLabel, value: SuggestionItem): JPanel {
|
||||
val item = value as PersonaActionItem
|
||||
return createPanel(
|
||||
|
|
@ -133,7 +133,7 @@ class PersonaItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane)
|
|||
}
|
||||
}
|
||||
|
||||
class DocumentationItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(textPane) {
|
||||
class DocumentationItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) {
|
||||
override fun render(component: JLabel, value: SuggestionItem): JPanel {
|
||||
val item = value as DocumentationActionItem
|
||||
return createPanel(
|
||||
|
|
@ -146,7 +146,7 @@ class DocumentationItemRenderer(textPane: CustomTextPane) : BaseItemRenderer(tex
|
|||
}
|
||||
}
|
||||
|
||||
class RendererFactory(private val textPane: CustomTextPane) {
|
||||
class RendererFactory(private val textPane: PromptTextField) {
|
||||
fun getRenderer(item: SuggestionItem): ItemRenderer {
|
||||
return when (item) {
|
||||
is FileActionItem -> FileItemRenderer(textPane)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ object SuggestionItemRendererTextUtils {
|
|||
val lastAtIndex = this.lastIndexOf('@')
|
||||
if (lastAtIndex == -1) return null
|
||||
|
||||
val lastColonIndex = this.lastIndexOf(':')
|
||||
if (lastColonIndex == -1) return null
|
||||
|
||||
return this.substring(lastColonIndex + 1).takeIf { it.isNotEmpty() }
|
||||
return this.substring(lastAtIndex + 1).takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun String.truncate(maxWidth: Int, truncateFromStart: Boolean = false): String {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.suggestion.renderer
|
||||
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.ui.textarea.CustomTextPane
|
||||
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
|
||||
import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import javax.swing.*
|
||||
|
||||
class SuggestionListCellRenderer(textPane: CustomTextPane) : DefaultListCellRenderer() {
|
||||
class SuggestionListCellRenderer(textPane: PromptTextField) : DefaultListCellRenderer() {
|
||||
private val rendererFactory = RendererFactory(textPane)
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue