From f7c49f5f9022e2022f7578951a7d570484ea929d Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Thu, 23 Jan 2025 15:10:23 +0000 Subject: [PATCH] feat: option to disable open file chat tag and other minor improvements --- .../chat/ChatToolWindowTabPanel.java | 4 +- .../codegpt/toolwindow/chat/MessageBuilder.kt | 25 ---- .../codegpt/ui/textarea/PromptTextField.kt | 1 - .../ui/textarea/TagProcessorFactory.kt | 24 ++-- .../codegpt/ui/textarea/UserInputPanel.kt | 2 +- .../codegpt/ui/textarea/header/HeaderTag.kt | 114 +++++++++-------- .../textarea/header/UserInputHeaderPanel.kt | 118 +++++++++--------- 7 files changed, 133 insertions(+), 155 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 3b853477..c57dcd33 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -308,9 +308,7 @@ public class ChatToolWindowTabPanel implements Disposable { } private Unit handleSubmit(String text, List appliedTags) { - var messageBuilder = new MessageBuilder(project, text) - .withSelectedEditorContent() - .withInlays(appliedTags); + var messageBuilder = new MessageBuilder(project, text).withInlays(appliedTags); List referencedFiles = getReferencedFiles(); if (!referencedFiles.isEmpty()) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt index 11d4f080..7f317114 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt @@ -1,25 +1,15 @@ package ee.carlrobert.codegpt.toolwindow.chat -import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.ReferencedFile import ee.carlrobert.codegpt.conversations.message.Message import ee.carlrobert.codegpt.ui.textarea.TagProcessorFactory import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails -import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor class MessageBuilder(private val project: Project, private val text: String) { private val message = Message("") - private var editorContent: String = "" private var inlayContent: String = "" - fun withSelectedEditorContent(): MessageBuilder { - getSelectedEditor(project)?.let { editor -> - editorContent = processEditorSelectedText(editor) - } - return this - } - fun withInlays(appliedTags: List): MessageBuilder { if (appliedTags.isNotEmpty()) { inlayContent = processTags(message, appliedTags) @@ -42,10 +32,6 @@ class MessageBuilder(private val project: Project, private val text: String) { fun build(): Message { message.prompt = buildString { append(text) - if (editorContent.isNotBlank()) { - append("\n\n") - append(editorContent) - } if (inlayContent.isNotBlank()) { append("\n") append(inlayContent) @@ -54,17 +40,6 @@ class MessageBuilder(private val project: Project, private val text: String) { return message } - private fun processEditorSelectedText(editor: Editor): String { - return editor.selectionModel.selectedText?.let { selectedText -> - if (selectedText.isBlank()) return "" - - val fileExtension = editor.virtualFile?.name?.substringAfterLast('.', "") ?: "" - editor.selectionModel.removeSelection() - - "```$fileExtension\n$selectedText\n```" - } ?: "" - } - private fun processTags( message: Message, tags: List diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index df07ba99..871dc500 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -69,7 +69,6 @@ class PromptTextField( } override fun dispose() { - clear() suggestionsPopupManager.hidePopup() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt index 39a11159..ce1b1429 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt @@ -7,9 +7,7 @@ import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.conversations.message.Message import ee.carlrobert.codegpt.ui.textarea.header.* -import ee.carlrobert.codegpt.util.EditorUtil import ee.carlrobert.codegpt.util.GitUtil -import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension import git4idea.GitCommit object TagProcessorFactory { @@ -17,7 +15,7 @@ object TagProcessorFactory { fun getProcessor(project: Project, tagDetails: HeaderTagDetails): TagProcessor { return when (tagDetails) { is FileTagDetails -> FileTagProcessor() - is SelectionTagDetails -> SelectionTagProcessor(project) + is SelectionTagDetails -> SelectionTagProcessor() is DocumentationTagDetails -> DocumentationTagProcessor() is PersonaTagDetails -> PersonaTagProcessor() is FolderTagDetails -> FolderTagProcessor() @@ -45,23 +43,25 @@ class FileTagProcessor : TagProcessor { } } -class SelectionTagProcessor(private val project: Project) : TagProcessor { +class SelectionTagProcessor : TagProcessor { override fun process( message: Message, tagDetails: HeaderTagDetails, promptBuilder: StringBuilder ) { - if (tagDetails !is SelectionTagDetails) { + val selectionModel = (tagDetails as? SelectionTagDetails)?.selectionModel ?: return + if (!selectionModel.hasSelection() || tagDetails.virtualFile == null) { return } - EditorUtil.getSelectedEditor(project)?.let { selectedEditor -> - val fileExtension = getFileExtension(selectedEditor.virtualFile.name) - promptBuilder - .append("\n```$fileExtension\n") - .append(tagDetails.selectedText) - .append("\n```\n") - } + promptBuilder + .append("\n```${tagDetails.virtualFile?.extension}\n") + .append(selectionModel.selectedText) + .append("\n```\n") + + tagDetails.virtualFile = null + tagDetails.selectionModel = null + selectionModel.removeSelection() } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt index 5f046cab..26d817b5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt @@ -116,7 +116,7 @@ class UserInputPanel( } fun addSelection(editorFile: VirtualFile, selectionModel: SelectionModel) { - addTag(SelectionTagDetails(editorFile, selectionModel, selectionModel.selectedText)) + addTag(SelectionTagDetails(editorFile, selectionModel)) promptTextField.requestFocusInWindow() selectionModel.removeSelection() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt index 6984358d..46c45e95 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt @@ -1,8 +1,8 @@ package ee.carlrobert.codegpt.ui.textarea.header import com.intellij.icons.AllIcons +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.SelectionModel import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.fileEditor.FileEditorManager @@ -15,7 +15,8 @@ import com.jetbrains.rd.util.UUID import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.settings.prompts.PersonaDetails import ee.carlrobert.codegpt.ui.DocumentationDetails -import ee.carlrobert.codegpt.util.EditorUtil +import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor +import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditorFile import java.awt.Cursor import java.awt.FlowLayout import java.awt.Graphics @@ -50,7 +51,6 @@ data class FileTagDetails(var virtualFile: VirtualFile, override var selected: B data class SelectionTagDetails( var virtualFile: VirtualFile?, var selectionModel: SelectionModel?, - var selectedText: String? ) : HeaderTagDetails( "${virtualFile?.name} (${selectionModel?.selectionStartPosition?.line}:${selectionModel?.selectionEndPosition?.line})", @@ -74,7 +74,9 @@ data class FolderTagDetails(var folder: VirtualFile) : class WebTagDetails : HeaderTagDetails("Web", AllIcons.General.Web) -abstract class HeaderTag(val tagDetails: HeaderTagDetails, private val selectable: Boolean = true) : +class EmptyTagDetails : HeaderTagDetails("") + +abstract class HeaderTag(var tagDetails: HeaderTagDetails, private val selectable: Boolean = true) : JPanel() { val id: UUID = tagDetails.id @@ -91,23 +93,22 @@ abstract class HeaderTag(val tagDetails: HeaderTagDetails, private val selectabl setupUI() } - abstract fun onClose() - abstract fun onSelect(tagDetails: HeaderTagDetails) - fun updateLabel(text: String, icon: Icon) { + abstract fun onClose() + + fun update(text: String, icon: Icon? = null) { label.text = text - label.icon = IconUtil.scale(icon, null, 0.65f) - } - - open fun select() { - onSelect(tagDetails) - - if (!tagDetails.selected) { - tagDetails.selected = true - closeButton.isVisible = true - label.foreground = service().globalScheme.defaultForeground + icon?.let { + label.icon = IconUtil.scale(it, null, 0.65f) } + closeButton.isVisible = tagDetails.selected + label.foreground = if (tagDetails.selected) { + service().globalScheme.defaultForeground + } else { + JBUI.CurrentTheme.Label.disabledForeground(false) + } + repaint() } override fun paintComponent(g: Graphics) { @@ -138,82 +139,85 @@ abstract class HeaderTag(val tagDetails: HeaderTagDetails, private val selectabl add(label) add(closeButton) - if (selectable) { - addMouseListener(object : MouseAdapter() { - override fun mousePressed(e: MouseEvent) { - select() - repaint() + addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + if (!selectable) { + return } - }) - } + + onSelect(tagDetails) + } + }) } } abstract class SelectedFileHeaderTag( private val project: Project, - var virtualFile: VirtualFile? = project.getSelectedEditorFile() + var virtualFile: VirtualFile? = getSelectedEditorFile(project) ) : HeaderTag( - HeaderTagDetails( - virtualFile?.name ?: "", - virtualFile?.fileType?.icon - ) + if (virtualFile == null) HeaderTagDetails("") + else FileTagDetails(virtualFile) ) { init { - isVisible = project.getSelectedEditorFile() != null + isVisible = getSelectedEditorFile(project) != null } override fun onSelect(tagDetails: HeaderTagDetails) { if (tagDetails is FileTagDetails) { - project.service().openFile(tagDetails.virtualFile) + if (tagDetails.selected) { + project.service().openFile(tagDetails.virtualFile) + } + tagDetails.selected = !tagDetails.selected + + update(tagDetails.virtualFile.name, tagDetails.virtualFile.fileType.icon) } } fun update(virtualFile: VirtualFile) { - this.virtualFile = virtualFile - isVisible = true - updateLabel(virtualFile.name, virtualFile.fileType.icon) + tagDetails = FileTagDetails(virtualFile) + + runInEdt { + isVisible = true + update(virtualFile.name, virtualFile.fileType.icon) + } } } -class SelectionHeaderTag( - private val project: Project, - var selectedEditor: Editor? = project.getSelectedEditor() -) : HeaderTag( - SelectionTagDetails( - selectedEditor?.virtualFile, - selectedEditor?.selectionModel, - selectedEditor?.selectionModel?.selectedText - ), +fun getDefaultSelectionTagDetails(project: Project): HeaderTagDetails { + val editor = getSelectedEditor(project) + val selectionModel = editor?.selectionModel + return if (selectionModel?.hasSelection() == true) { + SelectionTagDetails(editor.virtualFile, selectionModel) + } else { + EmptyTagDetails() + } +} + +class SelectionHeaderTag(project: Project) : HeaderTag( + getDefaultSelectionTagDetails(project), false ) { init { - isVisible = selectedEditor?.selectionModel?.hasSelection() ?: false + isVisible = tagDetails !is EmptyTagDetails } override fun onSelect(tagDetails: HeaderTagDetails) { } override fun onClose() { - selectedEditor?.selectionModel?.removeSelection() + (tagDetails as? SelectionTagDetails)?.selectionModel?.removeSelection() } fun update(virtualFile: VirtualFile, selectionModel: SelectionModel) { + tagDetails = SelectionTagDetails(virtualFile, selectionModel) isVisible = selectionModel.hasSelection() - updateLabel( + update( "${virtualFile.name}:${selectionModel.selectionStart}-${selectionModel.selectionEnd}", virtualFile.fileType.icon ) } } -fun Icon.scale() = IconUtil.scale(this, null, 0.65f) - -fun Project.getSelectedEditorFile(): VirtualFile? { - return this.getSelectedEditor()?.virtualFile -} - -fun Project.getSelectedEditor(): Editor? { - return EditorUtil.getSelectedEditor(this) -} \ No newline at end of file +fun Icon.scale() = IconUtil.scale(this, null, 0.65f) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt index 2e8b8c46..d1c47c15 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt @@ -1,8 +1,8 @@ package ee.carlrobert.codegpt.ui.textarea.header -import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorKind import com.intellij.openapi.editor.SelectionModel import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent @@ -17,6 +17,7 @@ import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier import ee.carlrobert.codegpt.ui.textarea.WrapLayout import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager import ee.carlrobert.codegpt.util.EditorUtil +import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditorFile import java.awt.FlowLayout import javax.swing.JPanel @@ -47,7 +48,7 @@ class UserInputHeaderPanel( foreground = JBUI.CurrentTheme.Label.disabledForeground() font = JBUI.Fonts.smallFont() border = JBUI.Borders.emptyLeft(2) - isVisible = project.getSelectedEditor() == null + isVisible = getSelectedEditor(project) == null } private val selectionHeaderTag = SelectionHeaderTag(project) @@ -60,24 +61,13 @@ class UserInputHeaderPanel( val selectedTags: MutableList = tags.filter { it.selected }.toMutableList() - val selectedFile = selectedFileHeaderTag.virtualFile - if (selectedFileHeaderTag.isVisible && selectedFile != null) { + val selectedFile = (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile + if (selectedFileHeaderTag.isVisible && selectedFileHeaderTag.tagDetails.selected && selectedFile != null) { selectedTags.add(FileTagDetails(selectedFile)) } - selectionHeaderTag.selectedEditor?.let { editor -> - val selectionFile = editor.virtualFile - if (!editor.isDisposed && selectionHeaderTag.isVisible && selectionFile != null) { - selectedTags.add( - runReadAction { - SelectionTagDetails( - selectionFile, - editor.selectionModel, - editor.selectionModel.selectedText - ) - } - ) - } + (selectionHeaderTag.tagDetails as? SelectionTagDetails)?.let { + selectedTags.add(it) } return selectedTags @@ -86,35 +76,11 @@ class UserInputHeaderPanel( fun addTag(tagDetails: HeaderTagDetails) { if (selectedFileHeaderTag.isVisible && tagDetails is FileTagDetails - && selectedFileHeaderTag.virtualFile == tagDetails.virtualFile + && (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile == tagDetails.virtualFile ) { return } - val tag = object : HeaderTag(tagDetails, tagDetails is FileTagDetails) { - override fun onSelect(tagDetails: HeaderTagDetails) { - if (tagDetails is FileTagDetails) { - if (tagDetails.selected) { - project.service().openFile(tagDetails.virtualFile) - } else { - selectedFileTags.add(tagDetails) - } - } - } - - override fun onClose() { - removeTag(tagDetails.id) - } - - override fun select() { - updateTagPosition(this) - super.select() - if (tags.filterIsInstance().filter { !it.selected }.size <= 2) { - addNextOpenFile() - } - } - } - if (tags.add(tagDetails)) { emptyText.isVisible = false @@ -122,6 +88,7 @@ class UserInputHeaderPanel( selectedFileTags.add(tagDetails) } + val tag = createTag(tagDetails) val lastSelectionTagIndex = getLastSelectedTagIndex() if (lastSelectionTagIndex != -1) { add(tag, lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1) @@ -142,6 +109,34 @@ class UserInputHeaderPanel( } } + private fun createTag(tagDetails: HeaderTagDetails) = + object : HeaderTag(tagDetails, tagDetails is FileTagDetails) { + override fun onSelect(tagDetails: HeaderTagDetails) { + tagDetails.selected = !tagDetails.selected + update(tagDetails.name, tagDetails.icon) + + if (tagDetails is FileTagDetails) { + if (tagDetails.selected) { + selectedFileTags.add(tagDetails) + updateTagPosition(this) + + val canAddNewTag = tags + .filter { it is FileTagDetails && !it.selected }.size <= 2 + if (canAddNewTag) { + addNextOpenFile() + } + } else { + project.service().openFile(tagDetails.virtualFile) + } + } + } + + override fun onClose() { + selectedFileTags.removeIf { it.id == tagDetails.id } + removeTag(tagDetails.id) + } + } + private fun updateTagPosition(tag: HeaderTag) { remove(tag) val lastSelectionTagIndex = getLastSelectedTagIndex() @@ -150,6 +145,8 @@ class UserInputHeaderPanel( } else { add(tag, max(getFirstUnselectedTagIndex(), TAG_INSERTION_OFFSET)) } + repaint() + revalidate() } private fun getFilteredHeaderTags(): List = components @@ -168,18 +165,22 @@ class UserInputHeaderPanel( private fun getFileTag(file: VirtualFile): FileTagDetails? = tags.filterIsInstance().find { it.virtualFile == file } + private fun isFileTagExists(file: VirtualFile): Boolean { + return getFileTag(file) != null || selectedFileHeaderTag.virtualFile == file + } + private fun getSortedOpenFiles(project: Project): MutableList = EditorUtil.getOpenLocalFiles(project) - .filterNot { it == selectedFileHeaderTag.virtualFile } + .filterNot { isFileTagExists(it) } .map { FileTagDetails(it) } .toMutableList() private fun addNextOpenFile() { - val file = EditorUtil.getOpenLocalFiles(project).firstOrNull { - !tags.filterIsInstance().any { tag -> tag.virtualFile == it } - && it != selectedFileHeaderTag.virtualFile - } ?: return - addTag(FileTagDetails(file, false)) + EditorUtil.getOpenLocalFiles(project) + .firstOrNull { !isFileTagExists(it) } + ?.let { + addTag(FileTagDetails(it, false)) + } } private fun removeFileTag(virtualFile: VirtualFile) { @@ -220,7 +221,7 @@ class UserInputHeaderPanel( add(selectedFileHeaderTag) if (tags.size <= 2) { - val selectedFile = EditorUtil.getSelectedEditor(project)?.virtualFile + val selectedFile = getSelectedEditor(project)?.virtualFile getSortedOpenFiles(project) .take(INITIAL_VISIBLE_FILES) .forEach { @@ -258,7 +259,7 @@ class UserInputHeaderPanel( private inner class EditorCreatedListener : EditorNotifier.Created { override fun editorCreated(editor: Editor) { editor.virtualFile?.let { editorFile -> - if (selectedFileHeaderTag.isVisible && selectedFileHeaderTag.virtualFile == editorFile) { + if (selectedFileHeaderTag.isVisible && (selectedFileHeaderTag.tagDetails as? FileTagDetails)?.virtualFile == editorFile) { return } @@ -269,11 +270,13 @@ class UserInputHeaderPanel( private inner class EditorReleasedListener : EditorNotifier.Released { override fun editorReleased(editor: Editor) { - removeFileTag(editor.virtualFile) + if (editor.editorKind == EditorKind.MAIN_EDITOR && !editor.isDisposed && editor.virtualFile != null) { + removeFileTag(editor.virtualFile) - if (tags.isEmpty() && project.getSelectedEditorFile() == null) { - selectedFileHeaderTag.isVisible = false - emptyText.isVisible = true + if (tags.isEmpty() && getSelectedEditorFile(project) == null) { + selectedFileHeaderTag.isVisible = false + emptyText.isVisible = true + } } } } @@ -299,12 +302,11 @@ class UserInputHeaderPanel( private inner class IncludedFilesListener : IncludeFilesInContextNotifier { override fun filesIncluded(includedFiles: MutableList) { - val selectedEditorFile = getSelectedEditorFile(project) - includedFiles.forEach { - if (getFileTag(it) == null && selectedEditorFile != it) { + includedFiles + .filterNot { isFileTagExists(it) } + .forEach { addTag(FileTagDetails(it)) } - } } } } \ No newline at end of file