From 831e2a4e6c283bdfbb96b8d5dd93d102ec3c5397 Mon Sep 17 00:00:00 2001 From: "a.iudin" Date: Fri, 30 May 2025 19:08:43 +0300 Subject: [PATCH 1/4] Replace LookupImpl and LookupUtil with custom JBPopup-based suggestion popup --- .../psistructure/PsiStructureProvider.kt | 14 +- .../codegpt/ui/textarea/PromptTextField.kt | 610 ++++++++++++++---- .../PromptTextFieldEventDispatcher.kt | 2 - .../textarea/header/UserInputHeaderPanel.kt | 4 +- .../ui/textarea/header/tag/TagManager.kt | 16 - .../ui/textarea/lookup/LoadingLookupItem.kt | 24 + .../codegpt/ui/textarea/lookup/LookupItem.kt | 4 +- .../codegpt/ui/textarea/lookup/LookupUtil.kt | 18 - .../textarea/lookup/group/FilesGroupItem.kt | 164 ++++- .../textarea/lookup/group/FoldersGroupItem.kt | 27 +- .../ui/textarea/lookup/group/GitGroupItem.kt | 36 +- .../textarea/popup/LookupListCellRenderer.kt | 45 ++ .../ui/textarea/popup/LookupListModel.kt | 11 + .../util/coroutines/CoroutineExtensions.kt | 13 + .../resources/messages/codegpt.properties | 1 + 15 files changed, 765 insertions(+), 224 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LoadingLookupItem.kt delete mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupUtil.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListModel.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/coroutines/CoroutineExtensions.kt diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt index aa13e7cb3..f90b4f209 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt @@ -5,13 +5,9 @@ import com.intellij.openapi.application.ReadAction import com.intellij.psi.PsiFile import com.intellij.util.io.await import ee.carlrobert.codegpt.psistructure.models.ClassStructure -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive +import ee.carlrobert.codegpt.util.coroutines.runCatchingCancellable +import kotlinx.coroutines.* import org.jetbrains.kotlin.psi.KtFile -import kotlin.coroutines.cancellation.CancellationException class PsiStructureProvider { @@ -26,7 +22,7 @@ class PsiStructureProvider { while (result == null && attempts < maxAttempts) { attempts++ - try { + runCatchingCancellable { val project = psiFiles .map { it.project } .firstOrNull { !it.isDisposed } ?: error("Project not available") @@ -60,9 +56,7 @@ class PsiStructureProvider { .submit(Dispatchers.Default.asExecutor()) result = future.await() - } catch (e: CancellationException) { - throw e - } catch (_: Exception) { + }.onFailure { delay(DELAY_RESTART_READ_ACTION) } } 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 68592f71b..5be66744f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -1,12 +1,9 @@ package ee.carlrobert.codegpt.ui.textarea -import com.intellij.codeInsight.lookup.* -import com.intellij.codeInsight.lookup.impl.LookupImpl -import com.intellij.codeInsight.lookup.impl.PrefixChangeListener import com.intellij.ide.IdeEventQueue import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runUndoTransparentWriteAction import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor @@ -19,25 +16,41 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.fileTypes.FileTypes import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.EditorTextField import com.intellij.ui.JBColor +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBScrollPane import com.intellij.util.ui.JBUI import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager -import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem +import ee.carlrobert.codegpt.ui.textarea.lookup.* import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.GitCommitActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.group.* +import ee.carlrobert.codegpt.ui.textarea.popup.LookupListCellRenderer +import ee.carlrobert.codegpt.ui.textarea.popup.LookupListModel +import ee.carlrobert.codegpt.util.coroutines.runCatchingCancellable import kotlinx.coroutines.* +import java.awt.BorderLayout import java.awt.Dimension +import java.awt.GraphicsEnvironment +import java.awt.Point +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.util.* +import javax.swing.ListSelectionModel +import javax.swing.SwingUtilities +import kotlin.math.min class PromptTextField( private val project: Project, @@ -48,11 +61,16 @@ class PromptTextField( private val onSubmit: (String) -> Unit, ) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable { - private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private val coroutineScope = CoroutineScope(Dispatchers.EDT + SupervisorJob()) private var showSuggestionsJob: Job? = null + private var showLoadingDelayedJob: Job? = null + private var searchJob: Job? = null val dispatcherId: UUID = UUID.randomUUID() - var lookup: LookupImpl? = null + private var currentPopup: JBPopup? = null + private var currentPopupPanel: PopupPanel? = null + private var currentParentGroup: LookupGroupItem? = null + private var currentItems: List = emptyList() init { isOneLineMode = false @@ -62,9 +80,8 @@ class PromptTextField( override fun onEditorAdded(editor: Editor) { IdeEventQueue.getInstance().addDispatcher( - PromptTextFieldEventDispatcher(dispatcherId, onBackSpace, lookup) { - val shown = lookup?.let { it.isShown && !it.isLookupDisposed } == true - if (shown) { + PromptTextFieldEventDispatcher(dispatcherId, onBackSpace) { + if (currentPopup?.isVisible == true) { return@PromptTextFieldEventDispatcher } @@ -90,47 +107,377 @@ class PromptTextField( DocsGroupItem(tagManager), MCPGroupItem(), WebActionItem(tagManager) - ) - .filter { it.enabled } - .map { it.createLookupElement() } - .toTypedArray() + ).filter { it.enabled } + + withContext(Dispatchers.EDT) { + editor?.let { showPopupLookup(it, lookupItems) } + } + } + + private fun showPopupLookup(editor: Editor, items: List) { + currentPopup?.cancel() + currentItems = items + + val popupPanel = createPopupPanel(items, null) + currentPopupPanel = popupPanel + + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(popupPanel, popupPanel.itemsList) + .setFocusable(true) + .setRequestFocus(true) + .setResizable(true) + .setCancelOnClickOutside(true) + .setCancelOnWindowDeactivation(true) + .createPopup() + + val relativePoint = calculateOptimalPopupPosition(editor, popupPanel) + + currentPopup = popup + popup.show(relativePoint) + + runInEdt { + popupPanel.itemsList.requestFocus() + } + } + + private fun createPopupPanel(items: List, parentGroup: LookupGroupItem?): PopupPanel { + return PopupPanel(items, parentGroup) + } + + private inner class PopupPanel( + items: List, + val parentGroup: LookupGroupItem? + ) : JBPanel(BorderLayout()) { + + private val listModel = LookupListModel(items) + val itemsList = JBList(listModel) + private var currentParentGroup: LookupGroupItem? = parentGroup + + init { + setupList() + setupLayout() + updateSearchFromEditor() + } + + fun updateItems(newItems: List, newParentGroup: LookupGroupItem?) { + currentParentGroup = newParentGroup + updateListItems(newItems) + } - withContext(Dispatchers.Main) { - editor?.let { - showGroupLookup(it, lookupItems) + private fun setupList() { + itemsList.apply { + cellRenderer = LookupListCellRenderer() + selectionMode = ListSelectionModel.SINGLE_SELECTION + if (model.size > 0) selectedIndex = 0 + + addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + when (e.keyCode) { + KeyEvent.VK_ENTER, KeyEvent.VK_TAB -> { + selectCurrentItem() + e.consume() + } + + KeyEvent.VK_ESCAPE -> { + currentPopup?.cancel() + e.consume() + } + + KeyEvent.VK_UP -> { + if (selectedIndex > 0) { + selectedIndex -= 1 + } else if (selectedIndex == 0 && model.size > 0) { + selectedIndex = model.size - 1 + } + e.consume() + } + + KeyEvent.VK_DOWN -> { + if (selectedIndex < model.size - 1) { + selectedIndex += 1 + } else if (selectedIndex == model.size - 1) { + selectedIndex = 0 + } + e.consume() + } + + KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE -> { + redirectKeyToEditor(e) + } + + else -> { + if (e.keyChar.isLetterOrDigit() || e.keyChar.isWhitespace() || + e.keyChar == '.' || e.keyChar == '/' || e.keyChar == '_' || e.keyChar == '-' + ) { + redirectKeyToEditor(e) + } + } + } + } + }) + + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2) { + selectCurrentItem() + } + } + }) + } + } + + private fun setupLayout() { + border = JBUI.Borders.empty(5) + add(JBScrollPane(itemsList), BorderLayout.CENTER) + + addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + when (e.keyCode) { + KeyEvent.VK_ENTER, KeyEvent.VK_TAB -> { + if (itemsList.selectedIndex >= 0) { + selectCurrentItem() + e.consume() + } + } + + KeyEvent.VK_ESCAPE -> { + currentPopup?.cancel() + e.consume() + } + + else -> { + if (e.keyChar.isLetterOrDigit() || e.keyChar.isWhitespace() || + e.keyChar == '.' || e.keyChar == '/' || e.keyChar == '_' || e.keyChar == '-' + ) { + redirectKeyToEditor(e) + } + } + } + } + }) + } + + private fun redirectKeyToEditor(e: KeyEvent) { + editor?.let { editor -> + runInEdt { + runUndoTransparentWriteAction { + when (e.keyCode) { + KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE -> { + val offset = editor.caretModel.offset + if (offset > 0) { + editor.document.deleteString(offset - 1, offset) + } + } + + else -> { + if (e.keyChar != KeyEvent.CHAR_UNDEFINED && e.keyChar.isDefined()) { + val offset = editor.caretModel.offset + editor.document.insertString(offset, e.keyChar.toString()) + editor.caretModel.moveToOffset(offset + 1) + } + } + } + } + } + e.consume() + } + } + + override fun getPreferredSize(): Dimension { + val maxWidth = 450 + val minWidth = 300 + val minHeight = 150 + val maxHeight = 400 + + val listSize = itemsList.preferredSize + val contentWidth = listSize.width + 10 + val contentHeight = listSize.height + 10 + + val width = minOf(maxOf(contentWidth, minWidth), maxWidth) + val height = minOf(maxOf(contentHeight, minHeight), maxHeight) + + return Dimension(width, height) + } + + fun updateSearchFromEditor() { + editor?.let { editor -> + when (val result = getSearchTextAfterAt(editor)) { + is SearchTextResult.Found -> { + updateFilter(result.query) + } + + is SearchTextResult.Cancelled -> { + currentPopup?.cancel() + } + + is SearchTextResult.None -> { + currentPopup?.cancel() + } + } + } + } + + fun updateFilter(searchText: String) { + searchJob?.cancel() + if (searchText.isEmpty()) { + updateListItems(this@PromptTextField.currentItems) + return + } + + searchJob = coroutineScope.launch { + val currentParentGroup = currentParentGroup + if (currentParentGroup != null && searchText.length >= 2) { + showLoadingState() + + runCatchingCancellable { + val items = if (currentParentGroup is DynamicLookupGroupItem) { + currentParentGroup.updateLookupItems(searchText) + } else { + currentParentGroup.getLookupItems(searchText) + } + .distinctBy { it.displayName } + + updateListItems(items) + } + .onFailure { + updateListItems(emptyList()) + } + } else if (searchText.length >= 2) { + showLoadingState() + runCatchingCancellable { + val filteredItems = this@PromptTextField.currentItems + .flatMap { item -> + when (item) { + is DynamicLookupGroupItem -> item.getLookupItems(searchText).take(5) + is LookupGroupItem -> item.getLookupItems(searchText).take(5) + else -> listOf(item) + } + } + .distinctBy { it.displayName } + .filter { it.displayName.contains(searchText, ignoreCase = true) } + yield() + updateListItems(filteredItems) + } + .onFailure { + yield() + updateListItems(emptyList()) + } + } else { + val filteredItems = this@PromptTextField.currentItems.filter { + it.displayName.contains(searchText, ignoreCase = true) + } + updateListItems(filteredItems) + } + } + } + + private fun showLoadingState() { + showLoadingDelayedJob?.cancel() + showLoadingDelayedJob = coroutineScope.launch { + delay(SHOW_LOADING_DELAY) + val loadingItems = listOf(LoadingLookupItem()) + val model = LookupListModel(loadingItems) + itemsList.model = model + itemsList.selectedIndex = -1 + } + } + + private fun updateListItems(items: List) { + showLoadingDelayedJob?.cancel() + runInEdt { + val model = LookupListModel(items) + itemsList.model = model + if (model.size > 0) { + itemsList.selectedIndex = 0 + } + } + } + + private fun selectCurrentItem() { + val selectedIndex = itemsList.selectedIndex + if (selectedIndex >= 0) { + val selectedItem = (itemsList.model as LookupListModel).getElementAt(selectedIndex) + + if (selectedItem is LoadingLookupItem) { + return + } + + editor?.let { handleItemSelection(it, selectedItem) } } } } - private fun showGroupLookup(editor: Editor, lookupElements: Array) { - lookup = createLookup(editor, lookupElements, "") - lookup?.addLookupListener(object : LookupListener { - override fun itemSelected(event: LookupEvent) { - val lookupString = event.item?.lookupString ?: return - val suggestion = - event.item?.getUserData(LookupItem.KEY) ?: return + private sealed class SearchTextResult { + data class Found(val query: String) : SearchTextResult() + data object Cancelled : SearchTextResult() + data object None : SearchTextResult() + } + + private fun getSearchTextAfterAt(editor: Editor): SearchTextResult { + val text = editor.document.text + val caretOffset = editor.caretModel.offset + val atPos = text.lastIndexOf('@', caretOffset) + + if (atPos !in 0 until caretOffset) { + return SearchTextResult.None + } + val endIndex = min(caretOffset + 1, text.length) + val substring = text.substring(atPos + 1, endIndex) + + return if (substring.contains(" ") || substring.contains("\n")) { + SearchTextResult.Cancelled + } else { + SearchTextResult.Found(substring) + } + } + + private fun handleItemSelection(editor: Editor, item: LookupItem) { + when (item) { + is WebActionItem -> { val offset = editor.caretModel.offset - val start = offset - lookupString.length + val start = findAtSymbolPosition(editor) if (start >= 0) { runUndoTransparentWriteAction { editor.document.deleteString(start, offset) } } + onLookupAdded(item) + currentPopup?.cancel() + } + + is LookupGroupItem -> { + showSuggestionsJob?.cancel() + showSuggestionsJob = coroutineScope.launch { + val suggestions = item.getLookupItems() + if (suggestions.isEmpty()) { + return@launch + } - if (suggestion is WebActionItem) { - onLookupAdded(suggestion) + runInEdt { + updatePopupContent(suggestions, item) + } } + } - if (suggestion !is LookupGroupItem) return + is LookupActionItem -> { + replaceAtSymbol(editor, item) + onLookupAdded(item) + currentPopup?.cancel() + } + } + } - showSuggestionsJob?.cancel() - showSuggestionsJob = coroutineScope.launch { - showGroupSuggestions(suggestion) + private fun updatePopupContent(items: List, parentGroup: LookupGroupItem) { + currentPopup?.let { popup -> + if (popup.isVisible) { + currentPopupPanel?.let { panel -> + currentParentGroup = parentGroup + currentItems = items + panel.updateItems(items, parentGroup) + panel.itemsList.requestFocusInWindow() } } - }) - lookup?.refreshUi(false, true) - lookup?.showLookup() + } } private fun findAtSymbolPosition(editor: Editor): Int { @@ -144,102 +491,104 @@ class PromptTextField( return } - val lookupElements = suggestions.map { it.createLookupElement() }.toTypedArray() - - withContext(Dispatchers.Main) { - showSuggestionLookup(lookupElements, group) + withContext(Dispatchers.EDT) { + updatePopupContent(suggestions, group) } } - private fun createLookup( - editor: Editor, - lookupElements: Array, - searchText: String - ) = runReadAction { - LookupManager.getInstance(project).createLookup( - editor, - lookupElements, - searchText, - LookupArranger.DefaultArranger() - ) as LookupImpl + private fun showSuggestionPopup( + items: List, + parentGroup: LookupGroupItem + ) { + editor?.let { editor -> + currentPopup?.cancel() + currentParentGroup = parentGroup + + val popupPanel = createPopupPanel(items, parentGroup) + currentPopupPanel = popupPanel + + val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(popupPanel, popupPanel.itemsList) + .setFocusable(true) + .setRequestFocus(true) + .setResizable(true) + .setCancelOnClickOutside(true) + .setCancelOnWindowDeactivation(true) + .createPopup() + + val relativePoint = calculateOptimalPopupPosition(editor, popupPanel) + + currentPopup = popup + popup.show(relativePoint) + + runInEdt { + popupPanel.itemsList.requestFocus() + } + } } - private fun showSuggestionLookup( - lookupElements: Array, - parentGroup: LookupGroupItem, - filterText: String = "", - ) { - editor?.let { - lookup = createLookup(it, lookupElements, filterText) - lookup?.addLookupListener(object : LookupListener { - override fun itemSelected(event: LookupEvent) { - val lookupItem = event.item?.getUserData(LookupItem.KEY) ?: return - if (lookupItem !is LookupActionItem) return - - replaceAtSymbol(it, lookupItem) - onLookupAdded(lookupItem) - } + private fun calculateOptimalPopupPosition(editor: Editor, popupPanel: PopupPanel): RelativePoint { + val caretPosition = editor.caretModel.visualPosition + val caretPoint = editor.visualPositionToXY(caretPosition) + val editorComponent = editor.contentComponent - private fun replaceAtSymbol(editor: Editor, lookupItem: LookupItem) { - val offset = editor.caretModel.offset - val start = findAtSymbolPosition(editor) - if (start >= 0) { - runUndoTransparentWriteAction { - val shouldInsertDisplayName = lookupItem is FileActionItem - || lookupItem is FolderActionItem - || lookupItem is GitCommitActionItem - if (shouldInsertDisplayName) { - editor.document.deleteString(start, offset) - editor.document.insertString(start, lookupItem.displayName) - editor.caretModel.moveToOffset(start + lookupItem.displayName.length) - editor.markupModel.addRangeHighlighter( - start, - start + lookupItem.displayName.length, - HighlighterLayer.SELECTION, - TextAttributes().apply { - foregroundColor = JBColor(0x00627A, 0xCC7832) - }, - HighlighterTargetArea.EXACT_RANGE - ) - } else { - editor.document.deleteString(start, offset) - } - } - } - } - }) + val caretLocationOnScreen = Point(caretPoint.x, caretPoint.y) + SwingUtilities.convertPointToScreen(caretLocationOnScreen, editorComponent) - lookup?.addPrefixChangeListener(object : PrefixChangeListener { - override fun afterAppend(c: Char) { - showSuggestionsJob?.cancel() - showSuggestionsJob = coroutineScope.launch { - if (parentGroup is DynamicLookupGroupItem) { - val searchText = getSearchText() - if (searchText.length == 2) { - parentGroup.updateLookupList(lookup!!, searchText) - } - } - } - } + val popupSize = popupPanel.preferredSize + val lineHeight = editor.lineHeight + val margin = JBUI.scale(8) - override fun afterTruncate() { - if (parentGroup is DynamicLookupGroupItem) { - val searchText = getSearchText() - if (searchText.isEmpty()) { - showSuggestionLookup(lookupElements, parentGroup, filterText) - } - } - } + val screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment() + .defaultScreenDevice.defaultConfiguration.bounds - private fun getSearchText(): String { - val text = it.document.text - return text.substring(text.lastIndexOf("@") + 1) - } + val spaceBelow = screenBounds.height - caretLocationOnScreen.y - lineHeight + val spaceAbove = caretLocationOnScreen.y + + val showAbove = popupSize.height + margin in (spaceBelow + 1)..spaceAbove + + val finalY = if (showAbove) { + caretLocationOnScreen.y - popupSize.height - margin + } else { + caretLocationOnScreen.y + lineHeight + margin + } - }, this) + val screenPoint = Point(caretLocationOnScreen.x, finalY) - lookup?.refreshUi(false, true) - lookup?.showLookup() + val editorLocationOnScreen = Point(0, 0) + SwingUtilities.convertPointToScreen(editorLocationOnScreen, editorComponent) + + val relativeX = screenPoint.x - editorLocationOnScreen.x + val relativeY = screenPoint.y - editorLocationOnScreen.y + + return RelativePoint(editorComponent, Point(relativeX, relativeY)) + } + + private fun replaceAtSymbol(editor: Editor, lookupItem: LookupItem) { + val offset = editor.caretModel.offset + val start = findAtSymbolPosition(editor) + if (start >= 0) { + runUndoTransparentWriteAction { + val shouldInsertDisplayName = lookupItem is FileActionItem + || lookupItem is FolderActionItem + || lookupItem is GitCommitActionItem + if (shouldInsertDisplayName) { + editor.document.deleteString(start, offset) + editor.document.insertString(start, lookupItem.displayName) + editor.caretModel.moveToOffset(start + lookupItem.displayName.length) + editor.markupModel.addRangeHighlighter( + start, + start + lookupItem.displayName.length, + HighlighterLayer.SELECTION, + TextAttributes().apply { + foregroundColor = JBColor(0x00627A, 0xCC7832) + }, + HighlighterTargetArea.EXACT_RANGE + ) + } else { + editor.document.deleteString(start, offset) + } + } } } @@ -256,7 +605,10 @@ class PromptTextField( } override fun dispose() { + searchJob?.cancel() showSuggestionsJob?.cancel() + currentPopup?.cancel() + currentPopupPanel = null } private fun setupDocumentListener(editor: EditorEx) { @@ -270,6 +622,12 @@ class PromptTextField( showSuggestionsJob = coroutineScope.launch { showGroupLookup() } + } else { + currentPopup?.let { popup -> + if (popup.isVisible) { + currentPopupPanel?.updateSearchFromEditor() + } + } } } }, this) @@ -291,4 +649,12 @@ class PromptTextField( return project.service() .getToolWindow("ProxyAI")?.component?.visibleRect?.height ?: 400 } + + private companion object { + /** + * Delay in milliseconds before showing the loading indicator + * to avoid flicker for quick operations. + */ + const val SHOW_LOADING_DELAY = 150L + } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldEventDispatcher.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldEventDispatcher.kt index f806d7f58..804ecf439 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldEventDispatcher.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldEventDispatcher.kt @@ -1,6 +1,5 @@ package ee.carlrobert.codegpt.ui.textarea -import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.ide.IdeEventQueue import com.intellij.openapi.application.runUndoTransparentWriteAction import com.intellij.openapi.util.TextRange @@ -16,7 +15,6 @@ import java.util.* class PromptTextFieldEventDispatcher( private val dispatcherId: UUID, private val onBackSpace: () -> Unit, - private val lookup: LookupImpl?, private val onSubmit: (KeyEvent) -> Unit ) : IdeEventQueue.EventDispatcher { 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 77d51cb1d..50e1c513b 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 @@ -258,8 +258,8 @@ class UserInputHeaderPanel( private inner class FileSelectionListener : FileEditorManagerListener { override fun selectionChanged(event: FileEditorManagerEvent) { event.newFile?.let { newFile -> - val containsTag = tagManager.getTags() - .none { it is EditorTagDetails && it.virtualFile == newFile } + val tags = tagManager.getTags() + val containsTag = !tags.contains(EditorTagDetails(newFile)) if (containsTag) { tagManager.addTag(EditorTagDetails(newFile).apply { selected = false }) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt index 4aee1f10a..b062fecb6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt @@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.ui.textarea.header.tag import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service -import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.configuration.ConfigurationStateListener import java.util.concurrent.CopyOnWriteArraySet @@ -36,21 +35,6 @@ class TagManager(parentDisposable: Disposable) { fun getTags(): Set = synchronized(this) { tags.toSet() } - fun containsTag(file: VirtualFile): Boolean = tags.any { - // TODO: refactor - if (it is SelectionTagDetails) { - it.virtualFile == file - } else if (it is FileTagDetails) { - it.virtualFile == file - } else if (it is EditorSelectionTagDetails) { - it.virtualFile == file - } else if (it is EditorTagDetails) { - it.virtualFile == file - } else { - false - } - } - fun addTag(tagDetails: TagDetails) { val wasAdded = synchronized(this) { if (!service().state.chatCompletionSettings.editorContextTagEnabled diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LoadingLookupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LoadingLookupItem.kt new file mode 100644 index 000000000..ec394ab7a --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LoadingLookupItem.kt @@ -0,0 +1,24 @@ +package ee.carlrobert.codegpt.ui.textarea.lookup + +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementPresentation +import com.intellij.icons.AllIcons +import ee.carlrobert.codegpt.CodeGPTBundle +import javax.swing.Icon + +class LoadingLookupItem : AbstractLookupItem() { + override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.loading.displayName") + override val icon: Icon = AllIcons.Process.Step_1 + + override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) { + presentation.icon = icon + presentation.itemText = displayName + presentation.isItemTextBold = false + presentation.isItemTextItalic = true + presentation.itemTextForeground = com.intellij.ui.JBColor.GRAY + } + + override fun getLookupString(): String { + return "loading_search" + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupItem.kt index fa985d411..0cf1a172e 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupItem.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.ui.textarea.lookup import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation -import com.intellij.codeInsight.lookup.impl.LookupImpl + import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import ee.carlrobert.codegpt.ui.textarea.UserInputPanel @@ -27,7 +27,7 @@ interface LookupGroupItem : LookupItem { } interface DynamicLookupGroupItem : LookupGroupItem { - suspend fun updateLookupList(lookup: LookupImpl, searchText: String) + suspend fun updateLookupItems(searchText: String): List } interface LookupActionItem : LookupItem { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupUtil.kt deleted file mode 100644 index a3346b7e3..000000000 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/LookupUtil.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ee.carlrobert.codegpt.ui.textarea.lookup - -import com.intellij.codeInsight.completion.PrefixMatcher -import com.intellij.codeInsight.completion.PrioritizedLookupElement -import com.intellij.codeInsight.lookup.impl.LookupImpl - -object LookupUtil { - - fun addLookupItem(lookup: LookupImpl, lookupItem: LookupItem, priority: Double = 5.0) { - if (!lookup.isLookupDisposed) { - lookup.addItem( - PrioritizedLookupElement.withPriority(lookupItem.createLookupElement(), priority), - PrefixMatcher.ALWAYS_TRUE - ) - lookup.refreshUi(true, true) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt index 74fac7264..aef354414 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt @@ -1,21 +1,19 @@ package ee.carlrobert.codegpt.ui.textarea.lookup.group -import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.icons.AllIcons import com.intellij.openapi.application.readAction -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.isFile import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager -import ee.carlrobert.codegpt.ui.textarea.header.tag.TagUtil import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupUtil +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.IncludeOpenFilesActionItem import kotlinx.coroutines.Dispatchers @@ -29,37 +27,151 @@ class FilesGroupItem( override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.files.displayName") override val icon = AllIcons.FileTypes.Any_type - override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) { - withContext(Dispatchers.Default) { - project.service().iterateContent { - if (!it.isDirectory && !containsTag(it)) { - val actionItem = FileActionItem(project, it) - runInEdt { - LookupUtil.addLookupItem(lookup, actionItem) - } - } - true - } - } + override suspend fun updateLookupItems(searchText: String): List { + return getFileItems(searchText) } override suspend fun getLookupItems(searchText: String): List { - return readAction { + return getFileItems(searchText) + } + + private suspend fun getFileItems(searchText: String): List { + return withContext(Dispatchers.IO) { + val fileEditorManager = project.service() val projectFileIndex = project.service() - project.service().openFiles - .filter { projectFileIndex.isInContent(it) && !containsTag(it) } - .toFileSuggestions() + + val (activeFiles, openFiles) = readAction { + val selectedFiles = fileEditorManager.selectedFiles + .filter { isValidFile(it, searchText, projectFileIndex) } + + val openFiles = fileEditorManager.openFiles.toList() + + val otherFiles = openFiles + .filter { it !in selectedFiles && isValidFile(it, searchText, projectFileIndex) } + + Pair(selectedFiles, otherFiles) + } + + val editorFilesCount = activeFiles.size + openFiles.size + val needFromFileSystem = maxOf(0, 30 - editorFilesCount) + + val filesFromSystem = mutableListOf() + if (needFromFileSystem > 0) { + val editorFilesSet = (activeFiles + openFiles).toSet() + + readAction { + projectFileIndex.iterateContent( + /* processor = */ { file -> + if (filesFromSystem.size >= needFromFileSystem) { + false + } else { + if (!editorFilesSet.contains(file)) { + filesFromSystem.add(file) + } + true + } + }, + /* filter = */ { file -> + !file.isDirectory && + isValidProjectFile(file, projectFileIndex) && + !containsTag(file) && + (searchText.isEmpty() || file.name.contains(searchText, ignoreCase = true)) + } + ) + } + } + + val allFiles = activeFiles + openFiles + filesFromSystem + + val result = allFiles + .map { FileActionItem(project, it) } + .toMutableList() + + if (searchText.isEmpty()) { + result.add(IncludeOpenFilesActionItem()) + } + + result.toList() } } + private fun isValidProjectFile(file: VirtualFile, projectFileIndex: ProjectFileIndex): Boolean { + return file.isFile && + !isExcludedFile(file) && + projectFileIndex.isInContent(file) && + !projectFileIndex.isInLibraryClasses(file) && + !projectFileIndex.isInLibrarySource(file) && + !projectFileIndex.isInGeneratedSources(file) + } + + private fun isExcludedFile(file: VirtualFile): Boolean { + return file.extension?.lowercase() in EXCLUDED_EXTENSIONS + } + + private fun isValidFile(file: VirtualFile, searchText: String, projectFileIndex: ProjectFileIndex): Boolean { + return isValidProjectFile(file, projectFileIndex) && + !containsTag(file) && + (searchText.isEmpty() || file.name.contains(searchText, ignoreCase = true)) + } + private fun containsTag(file: VirtualFile): Boolean { - return tagManager.containsTag(file) + val tags = tagManager.getTags() + return tags.contains(FileTagDetails(file)) } - private fun Iterable.toFileSuggestions(): List { - val selectedFileTags = TagUtil.getExistingTags(project, FileTagDetails::class.java) - return filter { file -> selectedFileTags.none { it.virtualFile == file } } - .take(10) - .map { FileActionItem(project, it) } + listOf(IncludeOpenFilesActionItem()) + private companion object { + val COMPILED_EXTENSIONS = setOf( + // Java/JVM languages + "class", "jar", "war", "ear", "aar", + + // C/C++/Objective-C + "o", "obj", "so", "dll", "dylib", "a", "lib", "framework", + + // .NET/C# + "exe", "pdb", "mdb", + + // Python + "pyc", "pyo", "pyd", + + // Rust + "rlib", + + // Go (compiled binaries often have no extension, but some cases) + "a", + + // Android + "dex", "apk", + + // iOS + "ipa", + + // Pascal/Delphi + "dcu", "dcp", + + // PHP + "phar", + + // Archives and packages + "zip", "tar", "gz", "bz2", "xz", "7z", "rar", + + // Other binary formats + "bin", "dat", "dump" + ) + + val TEMPORARY_EXTENSIONS = setOf( + // Backup files + "bak", "backup", "tmp", "temp", "swp", "swo", + + // IDE/Editor temporary files + "idea", "iml", "ipr", "iws", + + // OS temporary files + "ds_store", "thumbs.db", "desktop.ini", + + // Build artifacts + "log", "cache" + ) + + val EXCLUDED_EXTENSIONS = COMPILED_EXTENSIONS + TEMPORARY_EXTENSIONS } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt index 79622e5e1..335751fa8 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt @@ -1,17 +1,16 @@ package ee.carlrobert.codegpt.ui.textarea.lookup.group -import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.icons.AllIcons -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorTagDetails import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupUtil +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -24,16 +23,20 @@ class FoldersGroupItem( override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.folders.displayName") override val icon = AllIcons.Nodes.Folder - override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) { - withContext(Dispatchers.Default) { + override suspend fun updateLookupItems(searchText: String): List { + return withContext(Dispatchers.IO) { + val items = mutableListOf() + val tags = tagManager.getTags() project.service().iterateContent { - if (it.isDirectory && !it.name.startsWith(".") && !tagManager.containsTag(it)) { - runInEdt { - LookupUtil.addLookupItem(lookup, FolderActionItem(project, it)) - } + if (it.isDirectory && !it.name.startsWith(".") && + !tags.contains(EditorTagDetails(it)) && + (searchText.isEmpty() || it.name.contains(searchText, ignoreCase = true)) + ) { + items.add(FolderActionItem(project, it)) } - true + items.size < 50 } + items } } @@ -51,8 +54,10 @@ class FoldersGroupItem( private suspend fun getProjectFolders(project: Project) = withContext(Dispatchers.IO) { val folders = mutableSetOf() + val tags = tagManager.getTags() project.service().iterateContent { file: VirtualFile -> - if (file.isDirectory && !file.name.startsWith(".") && !tagManager.containsTag(file)) { + if (file.isDirectory && !file.name.startsWith(".") && + !tags.contains(EditorTagDetails(file))) { val folderPath = file.path if (folders.none { it.path.startsWith(folderPath) }) { folders.removeAll { it.path.startsWith(folderPath) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/GitGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/GitGroupItem.kt index 29a3cd3c8..9bb84b787 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/GitGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/GitGroupItem.kt @@ -1,14 +1,11 @@ package ee.carlrobert.codegpt.ui.textarea.lookup.group -import com.intellij.codeInsight.lookup.impl.LookupImpl -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.Icons -import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem -import ee.carlrobert.codegpt.ui.textarea.lookup.LookupUtil +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.GitCommitActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.IncludeCurrentChangesActionItem import ee.carlrobert.codegpt.util.GitUtil @@ -21,28 +18,37 @@ class GitGroupItem(private val project: Project) : AbstractLookupGroupItem(), Dy override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.git.displayName") override val icon: Icon = Icons.VCS - override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) { - withContext(Dispatchers.Default) { - GitUtil.getProjectRepository(project)?.let { - GitUtil.visitRepositoryCommits(project, it) { commit -> + private var allAvailableItems: List = emptyList() + + override suspend fun updateLookupItems(searchText: String): List { + return withContext(Dispatchers.Default) { + val items = mutableListOf() + GitUtil.getProjectRepository(project)?.let { repository -> + GitUtil.visitRepositoryCommits(project, repository) { commit -> if (commit.id.asString().contains(searchText, true) || commit.fullMessage.contains(searchText, true) ) { - runInEdt { - LookupUtil.addLookupItem(lookup, GitCommitActionItem(commit)) - } + items.add(GitCommitActionItem(commit)) } + items.size < 50 // Ограничиваем количество результатов } } + items } } override suspend fun getLookupItems(searchText: String): List { return withContext(Dispatchers.Default) { - GitUtil.getProjectRepository(project)?.let { - val recentCommits = GitUtil.getAllRecentCommits(project, it, searchText) - .take(10) - .map { commit -> GitCommitActionItem(commit) } + GitUtil.getProjectRepository(project)?.let { repository -> + val recentCommits = if (searchText.isEmpty()) { + GitUtil.getAllRecentCommits(project, repository, "") + .take(10) + .map { commit -> GitCommitActionItem(commit) } + } else { + GitUtil.getAllRecentCommits(project, repository, searchText) + .take(10) + .map { commit -> GitCommitActionItem(commit) } + } listOf(IncludeCurrentChangesActionItem()) + recentCommits } ?: emptyList() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt new file mode 100644 index 000000000..df4b7202e --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt @@ -0,0 +1,45 @@ +package ee.carlrobert.codegpt.ui.textarea.popup + +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem +import java.awt.BorderLayout +import java.awt.Component +import javax.swing.JList +import javax.swing.JPanel +import javax.swing.ListCellRenderer + +class LookupListCellRenderer : ListCellRenderer { + + override fun getListCellRendererComponent( + list: JList, + value: LookupItem, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val panel = JPanel(BorderLayout()).apply { + border = JBUI.Borders.empty(4, 8) + } + + val component = SimpleColoredComponent().apply { + icon = value.icon + append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + isOpaque = false + } + + panel.add(component, BorderLayout.CENTER) + + if (isSelected) { + panel.background = list.selectionBackground + component.foreground = list.selectionForeground + } else { + panel.background = list.background + component.foreground = list.foreground + } + + panel.isOpaque = true + return panel + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListModel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListModel.kt new file mode 100644 index 000000000..304c47ab6 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListModel.kt @@ -0,0 +1,11 @@ +package ee.carlrobert.codegpt.ui.textarea.popup + +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem +import javax.swing.AbstractListModel + +class LookupListModel(private val items: List) : AbstractListModel() { + + override fun getSize(): Int = items.size + + override fun getElementAt(index: Int): LookupItem = items[index] +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/coroutines/CoroutineExtensions.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/coroutines/CoroutineExtensions.kt new file mode 100644 index 000000000..417b0fc61 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/coroutines/CoroutineExtensions.kt @@ -0,0 +1,13 @@ +package ee.carlrobert.codegpt.util.coroutines + +import kotlinx.coroutines.CancellationException + +inline fun runCatchingCancellable(action: () -> T): Result { + return try { + Result.success(action()) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index a60dc34d6..6f1f0fa17 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -343,3 +343,4 @@ toolwindow.chat.editor.action.autoApply.dialog.applyToOpenFile=Apply to Open Fil toolwindow.chat.editor.action.autoApply.dialog.cancel=Cancel toolwindow.chat.editor.action.autoApply.creatingFile=Creating new file... toolwindow.chat.editor.action.autoApply.fileCreationError=Error creating file: {0} +suggestionGroupItem.loading.displayName=Loading... From 885dfa13ad175a387e467aef3a5f22885d428d26 Mon Sep 17 00:00:00 2001 From: "a.iudin" Date: Mon, 2 Jun 2025 21:48:39 +0300 Subject: [PATCH 2/4] Refine lookup popup UI: remove type text, restyle list cells, and enhance prompt text field popup styling --- .../codegpt/ui/textarea/PromptTextField.kt | 81 ++++++++++++++++--- .../lookup/group/AbstractLookupGroupItem.kt | 3 - .../textarea/popup/LookupListCellRenderer.kt | 72 ++++++++++++++--- 3 files changed, 130 insertions(+), 26 deletions(-) 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 5be66744f..fffa647f0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -25,7 +25,9 @@ import com.intellij.ui.awt.RelativePoint import com.intellij.ui.components.JBList import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.scale.JBUIScale import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager @@ -39,10 +41,7 @@ import ee.carlrobert.codegpt.ui.textarea.popup.LookupListCellRenderer import ee.carlrobert.codegpt.ui.textarea.popup.LookupListModel import ee.carlrobert.codegpt.util.coroutines.runCatchingCancellable import kotlinx.coroutines.* -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.GraphicsEnvironment -import java.awt.Point +import java.awt.* import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.MouseAdapter @@ -128,6 +127,9 @@ class PromptTextField( .setResizable(true) .setCancelOnClickOutside(true) .setCancelOnWindowDeactivation(true) + .setShowBorder(true) + .setShowShadow(true) + .setBorderColor(JBColor.border()) .createPopup() val relativePoint = calculateOptimalPopupPosition(editor, popupPanel) @@ -170,6 +172,17 @@ class PromptTextField( selectionMode = ListSelectionModel.SINGLE_SELECTION if (model.size > 0) selectedIndex = 0 + background = UIUtil.getListBackground() + foreground = UIUtil.getListForeground() + selectionBackground = UIUtil.getListSelectionBackground(true) + selectionForeground = UIUtil.getListSelectionForeground(true) + + fixedCellHeight = JBUIScale.scale(20) + border = JBUI.Borders.empty() + + isFocusTraversalPolicyProvider = false + setFocusTraversalKeysEnabled(false) + addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { when (e.keyCode) { @@ -227,8 +240,22 @@ class PromptTextField( } private fun setupLayout() { - border = JBUI.Borders.empty(5) - add(JBScrollPane(itemsList), BorderLayout.CENTER) + background = UIUtil.getListBackground() + border = JBUI.Borders.empty(4) + + isFocusTraversalPolicyProvider = false + setFocusTraversalKeysEnabled(false) + + val scrollPane = JBScrollPane(itemsList).apply { + border = JBUI.Borders.empty() + viewport.background = UIUtil.getListBackground() + verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER + isFocusTraversalPolicyProvider = false + setFocusTraversalKeysEnabled(false) + } + + add(scrollPane, BorderLayout.CENTER) addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { @@ -257,6 +284,18 @@ class PromptTextField( }) } + override fun paintComponent(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + + g2.color = UIUtil.getListBackground() + g2.fillRoundRect(0, 0, width, height, JBUIScale.scale(8), JBUIScale.scale(8)) + } finally { + g2.dispose() + } + } + private fun redirectKeyToEditor(e: KeyEvent) { editor?.let { editor -> runInEdt { @@ -284,14 +323,15 @@ class PromptTextField( } override fun getPreferredSize(): Dimension { - val maxWidth = 450 - val minWidth = 300 - val minHeight = 150 - val maxHeight = 400 + val maxWidth = JBUIScale.scale(450) + val minWidth = JBUIScale.scale(300) + val minHeight = JBUIScale.scale(120) + val maxHeight = JBUIScale.scale(300) - val listSize = itemsList.preferredSize - val contentWidth = listSize.width + 10 - val contentHeight = listSize.height + 10 + val itemCount = itemsList.model.size + val itemHeight = JBUIScale.scale(20) + val contentHeight = (itemCount * itemHeight) + 8 + val contentWidth = calculateOptimalWidth() val width = minOf(maxOf(contentWidth, minWidth), maxWidth) val height = minOf(maxOf(contentHeight, minHeight), maxHeight) @@ -299,6 +339,21 @@ class PromptTextField( return Dimension(width, height) } + private fun calculateOptimalWidth(): Int { + var maxWidth = JBUIScale.scale(200) + val fontMetrics = itemsList.getFontMetrics(itemsList.font) + + for (i in 0 until itemsList.model.size) { + val item = itemsList.model.getElementAt(i) + val textWidth = fontMetrics.stringWidth(item.displayName) + val iconWidth = item.icon?.iconWidth ?: 0 + val totalWidth = textWidth + iconWidth + JBUIScale.scale(32) + maxWidth = maxOf(maxWidth, totalWidth) + } + + return maxWidth + } + fun updateSearchFromEditor() { editor?.let { editor -> when (val result = getSearchTextAfterAt(editor)) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/AbstractLookupGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/AbstractLookupGroupItem.kt index d603ae4eb..b409c7fc3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/AbstractLookupGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/AbstractLookupGroupItem.kt @@ -2,7 +2,6 @@ package ee.carlrobert.codegpt.ui.textarea.lookup.group import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation -import com.intellij.icons.AllIcons import ee.carlrobert.codegpt.ui.textarea.lookup.AbstractLookupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem @@ -11,8 +10,6 @@ abstract class AbstractLookupGroupItem : AbstractLookupItem(), LookupGroupItem { override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) { presentation.itemText = displayName presentation.icon = icon - presentation.setTypeText("", AllIcons.Icons.Ide.NextStep) - presentation.isTypeIconRightAligned = true presentation.isItemTextBold = false } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt index df4b7202e..6ee17bd04 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt @@ -1,17 +1,22 @@ package ee.carlrobert.codegpt.ui.textarea.popup +import com.intellij.icons.AllIcons +import com.intellij.ui.JBColor import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.scale.JBUIScale import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import ee.carlrobert.codegpt.ui.textarea.lookup.LoadingLookupItem +import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem import java.awt.BorderLayout import java.awt.Component -import javax.swing.JList -import javax.swing.JPanel -import javax.swing.ListCellRenderer +import java.awt.Dimension +import javax.swing.* class LookupListCellRenderer : ListCellRenderer { - + override fun getListCellRendererComponent( list: JList, value: LookupItem, @@ -20,26 +25,73 @@ class LookupListCellRenderer : ListCellRenderer { cellHasFocus: Boolean ): Component { val panel = JPanel(BorderLayout()).apply { - border = JBUI.Borders.empty(4, 8) + preferredSize = Dimension(list.width, ITEM_HEIGHT) + border = JBUI.Borders.empty(0, 0, 0, 0) } val component = SimpleColoredComponent().apply { icon = value.icon - append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + iconTextGap = ICON_TEXT_GAP isOpaque = false + ipad = JBUI.insets(TOP_BOTTOM_MARGIN, LEFT_MARGIN, TOP_BOTTOM_MARGIN, 0) + + when (value) { + is LoadingLookupItem -> { + append(value.displayName, SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES) + } + + is LookupGroupItem -> { + append(value.displayName, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES) + } + + else -> { + append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + } } panel.add(component, BorderLayout.CENTER) + if (value is LookupGroupItem) { + val arrowLabel = JLabel().apply { + icon = AllIcons.Icons.Ide.NextStep + horizontalAlignment = SwingConstants.CENTER + verticalAlignment = SwingConstants.CENTER + border = JBUI.Borders.empty(0, JBUIScale.scale(4), 0, RIGHT_MARGIN) + isOpaque = false + + if (isSelected) { + foreground = UIUtil.getListSelectionForeground(true) + } else { + foreground = UIUtil.getListForeground() + } + } + panel.add(arrowLabel, BorderLayout.EAST) + } else { + val spacer = Box.createHorizontalStrut(RIGHT_MARGIN + JBUIScale.scale(16)) + panel.add(spacer, BorderLayout.EAST) + } + if (isSelected) { - panel.background = list.selectionBackground - component.foreground = list.selectionForeground + panel.background = UIUtil.getListSelectionBackground(true) + component.foreground = UIUtil.getListSelectionForeground(true) } else { - panel.background = list.background - component.foreground = list.foreground + panel.background = UIUtil.getListBackground() + component.foreground = when (value) { + is LoadingLookupItem -> JBColor.GRAY + else -> UIUtil.getListForeground() + } } panel.isOpaque = true return panel } + + private companion object { + val ITEM_HEIGHT = JBUIScale.scale(20) + val ICON_TEXT_GAP = JBUIScale.scale(4) + val LEFT_MARGIN = JBUIScale.scale(8) + val RIGHT_MARGIN = JBUIScale.scale(8) + val TOP_BOTTOM_MARGIN = JBUIScale.scale(2) + } } \ No newline at end of file From db8ae36af93be58589db8d228ccbabf08d242064 Mon Sep 17 00:00:00 2001 From: "a.iudin" Date: Mon, 2 Jun 2025 22:19:51 +0300 Subject: [PATCH 3/4] Add disabled state support for lookup items in popup UI --- .../codegpt/ui/textarea/PromptTextField.kt | 36 +++++++++++++------ .../ui/textarea/lookup/group/MCPGroupItem.kt | 1 + .../textarea/popup/LookupListCellRenderer.kt | 27 +++++++------- 3 files changed, 42 insertions(+), 22 deletions(-) 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 fffa647f0..ebabcff30 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -106,7 +106,7 @@ class PromptTextField( DocsGroupItem(tagManager), MCPGroupItem(), WebActionItem(tagManager) - ).filter { it.enabled } + ) withContext(Dispatchers.EDT) { editor?.let { showPopupLookup(it, lookupItems) } @@ -197,19 +197,35 @@ class PromptTextField( } KeyEvent.VK_UP -> { - if (selectedIndex > 0) { - selectedIndex -= 1 - } else if (selectedIndex == 0 && model.size > 0) { - selectedIndex = model.size - 1 + var newIndex = selectedIndex - 1 + while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { + newIndex-- + } + if (newIndex < 0) { + newIndex = model.size - 1 + while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { + newIndex-- + } + } + if (newIndex >= 0) { + selectedIndex = newIndex } e.consume() } KeyEvent.VK_DOWN -> { - if (selectedIndex < model.size - 1) { - selectedIndex += 1 - } else if (selectedIndex == model.size - 1) { - selectedIndex = 0 + var newIndex = selectedIndex + 1 + while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { + newIndex++ + } + if (newIndex >= model.size) { + newIndex = 0 + while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { + newIndex++ + } + } + if (newIndex < model.size) { + selectedIndex = newIndex } e.consume() } @@ -453,7 +469,7 @@ class PromptTextField( if (selectedIndex >= 0) { val selectedItem = (itemsList.model as LookupListModel).getElementAt(selectedIndex) - if (selectedItem is LoadingLookupItem) { + if (selectedItem is LoadingLookupItem || !selectedItem.enabled) { return } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/MCPGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/MCPGroupItem.kt index 8c56d835e..cd0c5a5b1 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/MCPGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/MCPGroupItem.kt @@ -14,6 +14,7 @@ class MCPGroupItem : AbstractLookupGroupItem() { override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.mcp.displayName") override val icon: Icon = Icons.MCP + override val enabled: Boolean = false override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) { super.setPresentation(element, presentation) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt index 6ee17bd04..7fea315a8 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/popup/LookupListCellRenderer.kt @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.ui.textarea.popup import com.intellij.icons.AllIcons +import com.intellij.openapi.util.IconLoader import com.intellij.ui.JBColor import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes @@ -30,20 +31,21 @@ class LookupListCellRenderer : ListCellRenderer { } val component = SimpleColoredComponent().apply { - icon = value.icon + icon = if (value.enabled) value.icon else value.icon?.let { IconLoader.getDisabledIcon(it) } iconTextGap = ICON_TEXT_GAP isOpaque = false ipad = JBUI.insets(TOP_BOTTOM_MARGIN, LEFT_MARGIN, TOP_BOTTOM_MARGIN, 0) - when (value) { - is LoadingLookupItem -> { + when { + !value.enabled -> { + append(value.displayName, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + value is LoadingLookupItem -> { append(value.displayName, SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES) } - - is LookupGroupItem -> { + value is LookupGroupItem -> { append(value.displayName, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES) } - else -> { append(value.displayName, SimpleTextAttributes.REGULAR_ATTRIBUTES) } @@ -54,16 +56,16 @@ class LookupListCellRenderer : ListCellRenderer { if (value is LookupGroupItem) { val arrowLabel = JLabel().apply { - icon = AllIcons.Icons.Ide.NextStep + icon = if (value.enabled) AllIcons.Icons.Ide.NextStep else IconLoader.getDisabledIcon(AllIcons.Icons.Ide.NextStep) horizontalAlignment = SwingConstants.CENTER verticalAlignment = SwingConstants.CENTER border = JBUI.Borders.empty(0, JBUIScale.scale(4), 0, RIGHT_MARGIN) isOpaque = false - if (isSelected) { + if (isSelected && value.enabled) { foreground = UIUtil.getListSelectionForeground(true) } else { - foreground = UIUtil.getListForeground() + foreground = if (value.enabled) UIUtil.getListForeground() else JBUI.CurrentTheme.Label.disabledForeground() } } panel.add(arrowLabel, BorderLayout.EAST) @@ -72,13 +74,14 @@ class LookupListCellRenderer : ListCellRenderer { panel.add(spacer, BorderLayout.EAST) } - if (isSelected) { + if (isSelected && value.enabled) { panel.background = UIUtil.getListSelectionBackground(true) component.foreground = UIUtil.getListSelectionForeground(true) } else { panel.background = UIUtil.getListBackground() - component.foreground = when (value) { - is LoadingLookupItem -> JBColor.GRAY + component.foreground = when { + !value.enabled -> JBUI.CurrentTheme.Label.disabledForeground() + value is LoadingLookupItem -> JBColor.GRAY else -> UIUtil.getListForeground() } } From f62342af25ed365ab5515f1d736e0ed2653b8b77 Mon Sep 17 00:00:00 2001 From: "a.iudin" Date: Tue, 3 Jun 2025 16:33:07 +0300 Subject: [PATCH 4/4] Refactor key event handling in suggestion list to use ActionMap/InputMap for navigation keys - Replace KeyAdapter logic for UP and DOWN arrow keys with ActionMap/InputMap actions - Retain KeyAdapter for ENTER, TAB, and ESCAPE keys - Improve code clarity and separation of concerns for key event handling - Prepare for better keyboard accessibility and future extensibility --- .../codegpt/ui/textarea/PromptTextField.kt | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) 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 ebabcff30..9e83a1e97 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -42,11 +42,10 @@ import ee.carlrobert.codegpt.ui.textarea.popup.LookupListModel import ee.carlrobert.codegpt.util.coroutines.runCatchingCancellable import kotlinx.coroutines.* import java.awt.* -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent +import java.awt.event.* import java.util.* +import javax.swing.AbstractAction +import javax.swing.KeyStroke import javax.swing.ListSelectionModel import javax.swing.SwingUtilities import kotlin.math.min @@ -183,6 +182,47 @@ class PromptTextField( isFocusTraversalPolicyProvider = false setFocusTraversalKeysEnabled(false) + inputMap.get(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0))?.also { actionUp -> + actionMap.put(actionUp, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + var newIndex = selectedIndex - 1 + while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { + newIndex-- + } + if (newIndex < 0) { + newIndex = model.size - 1 + while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { + newIndex-- + } + } + println("sssssss VK_UP $newIndex") + if (newIndex >= 0) { + selectedIndex = newIndex + } + } + }) + } + inputMap.get(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0))?.also { actionDown -> + actionMap.put(actionDown, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + var newIndex = selectedIndex + 1 + while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { + newIndex++ + } + if (newIndex >= model.size) { + newIndex = 0 + while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { + newIndex++ + } + } + println("sssssss VK_DOWN $newIndex") + if (newIndex < model.size) { + selectedIndex = newIndex + } + } + }) + } + addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { when (e.keyCode) { @@ -196,40 +236,6 @@ class PromptTextField( e.consume() } - KeyEvent.VK_UP -> { - var newIndex = selectedIndex - 1 - while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { - newIndex-- - } - if (newIndex < 0) { - newIndex = model.size - 1 - while (newIndex >= 0 && !listModel.getElementAt(newIndex).enabled) { - newIndex-- - } - } - if (newIndex >= 0) { - selectedIndex = newIndex - } - e.consume() - } - - KeyEvent.VK_DOWN -> { - var newIndex = selectedIndex + 1 - while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { - newIndex++ - } - if (newIndex >= model.size) { - newIndex = 0 - while (newIndex < model.size && !listModel.getElementAt(newIndex).enabled) { - newIndex++ - } - } - if (newIndex < model.size) { - selectedIndex = newIndex - } - e.consume() - } - KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE -> { redirectKeyToEditor(e) }