Skip to content

Commit 474c3b1

Browse files
committed
feat(WorkspaceFileSearchPopup): redesign file search UI
Refactor file search popup to match IntelliJ IDEA style with improved navigation, keyboard shortcuts, and better user experience. Rename ModelSelectorsManager to InputSelectorsManager for consistency.
1 parent 9017574 commit 474c3b1

File tree

3 files changed

+114
-64
lines changed

3 files changed

+114
-64
lines changed

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/AutoDevInputSection.kt

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,13 @@ class AutoDevInputSection(
2929
val disposable: Disposable?,
3030
showAgent: Boolean = true
3131
) : BorderLayoutPanel() {
32-
33-
// Event dispatcher
3432
val editorListeners = EventDispatcher.create(AutoDevInputListener::class.java)
35-
36-
// Manager components
3733
private val inputControlsManager = InputControlsManager(project, disposable, editorListeners)
38-
private val modelSelectorsManager = ModelSelectorsManager(project, showAgent)
34+
private val inputSelectorsManager = InputSelectorsManager(project, showAgent)
3935
private val fileWorkspaceManager = FileWorkspaceManager(project, disposable)
4036

41-
// UI panels
4237
private val inputPanel = BorderLayoutPanel()
4338

44-
// Convenience accessors
4539
val focusableComponent: JComponent get() = inputControlsManager.getFocusableComponent()
4640

4741
fun renderText(): String {
@@ -59,15 +53,11 @@ class AutoDevInputSection(
5953
}
6054

6155
init {
62-
// Initialize all managers
6356
inputControlsManager.initialize(this)
64-
val leftPanel = modelSelectorsManager.initialize()
57+
val leftPanel = inputSelectorsManager.initialize()
6558
val headerPanel = fileWorkspaceManager.initialize(inputControlsManager.input)
6659

67-
// Setup layout
6860
setupLayout(leftPanel, headerPanel)
69-
70-
// Setup listeners
7161
addListener(object : AutoDevInputListener {
7262
override fun editorAdded(editor: EditorEx) {
7363
this@AutoDevInputSection.initEditor()
@@ -91,7 +81,7 @@ class AutoDevInputSection(
9181
inputControlsManager.input.minimumSize = Dimension(inputControlsManager.input.minimumSize.width, 64)
9282
layoutPanel.addToLeft(leftPanel)
9383
} else {
94-
layoutPanel.addToLeft(modelSelectorsManager.modelSelector)
84+
layoutPanel.addToLeft(inputSelectorsManager.modelSelector)
9585
}
9686

9787
layoutPanel.addToCenter(horizontalGlue)
@@ -135,14 +125,14 @@ class AutoDevInputSection(
135125
}
136126

137127
// Agent management methods
138-
fun hasSelectedAgent(): Boolean = modelSelectorsManager.hasSelectedAgent()
128+
fun hasSelectedAgent(): Boolean = inputSelectorsManager.hasSelectedAgent()
139129

140-
fun getSelectedAgent(): CustomAgentConfig = modelSelectorsManager.getSelectedAgent()
130+
fun getSelectedAgent(): CustomAgentConfig = inputSelectorsManager.getSelectedAgent()
141131

142-
fun selectAgent(config: CustomAgentConfig) = modelSelectorsManager.selectAgent(config)
132+
fun selectAgent(config: CustomAgentConfig) = inputSelectorsManager.selectAgent(config)
143133

144134
fun resetAgent() {
145-
modelSelectorsManager.resetAgent()
135+
inputSelectorsManager.resetAgent()
146136
inputControlsManager.clearText()
147137
fileWorkspaceManager.clearWorkspace()
148138
}

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/ModelSelectorsManager.kt renamed to core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/InputSelectorsManager.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@ import javax.swing.JPanel
2020
/**
2121
* Manages model selector and agent selector components
2222
*/
23-
class ModelSelectorsManager(
23+
class InputSelectorsManager(
2424
private val project: Project,
2525
private val showAgent: Boolean = true
2626
) {
27-
// Model selector
2827
lateinit var modelSelector: ComboBox<ModelItem>
2928
private set
3029

31-
// Agent selector
3230
private val defaultRag: CustomAgentConfig = CustomAgentConfig("<Select Custom Agent>", "Normal")
3331
lateinit var customAgentBox: ComboBox<CustomAgentConfig>
3432

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/file/WorkspaceFileSearchPopup.kt

Lines changed: 106 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,48 @@ package cc.unitmesh.devti.gui.chat.ui.file
33
import cc.unitmesh.devti.util.canBeAdded
44
import com.intellij.openapi.fileEditor.impl.EditorHistoryManager
55
import com.intellij.openapi.project.Project
6+
import com.intellij.openapi.project.guessProjectDir
67
import com.intellij.openapi.roots.ProjectFileIndex
78
import com.intellij.openapi.ui.popup.JBPopup
89
import com.intellij.openapi.ui.popup.JBPopupFactory
10+
import com.intellij.openapi.ui.popup.JBPopupListener
11+
import com.intellij.openapi.ui.popup.LightweightWindowEvent
912
import com.intellij.openapi.vfs.VirtualFile
13+
import com.intellij.openapi.wm.IdeFocusManager
1014
import com.intellij.ui.JBColor
15+
import com.intellij.ui.SearchTextField
16+
import com.intellij.ui.SpeedSearchComparator
1117
import com.intellij.ui.awt.RelativePoint
1218
import com.intellij.ui.components.JBLabel
1319
import com.intellij.ui.components.JBList
20+
import com.intellij.ui.components.JBScrollPane
1421
import com.intellij.util.ui.JBUI
1522
import com.intellij.util.ui.UIUtil
1623
import org.jetbrains.annotations.NotNull
1724
import java.awt.BorderLayout
1825
import java.awt.Component
1926
import java.awt.Dimension
2027
import java.awt.Point
28+
import java.awt.event.KeyAdapter
29+
import java.awt.event.KeyEvent
2130
import java.awt.event.MouseAdapter
2231
import java.awt.event.MouseEvent
2332
import javax.swing.*
24-
import javax.swing.event.DocumentEvent
25-
import javax.swing.event.DocumentListener
2633

2734
class WorkspaceFileSearchPopup(
2835
private val project: Project,
2936
private val onFilesSelected: (List<VirtualFile>) -> Unit
3037
) {
3138
private var popup: JBPopup? = null
3239
private val fileListModel = DefaultListModel<FilePresentation>()
33-
private val fileList = JBList(fileListModel)
34-
private val searchField = JTextField()
40+
private val fileList = JBList(fileListModel).apply {
41+
cellRenderer = FileListCellRenderer()
42+
selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
43+
}
44+
private val searchField = SearchTextField()
3545
private val contentPanel = JPanel(BorderLayout())
3646
private val allProjectFiles = mutableListOf<FilePresentation>()
37-
private val minPopupSize = Dimension(435, 300)
47+
private val minPopupSize = Dimension(500, 400)
3848

3949
init {
4050
loadProjectFiles()
@@ -43,15 +53,18 @@ class WorkspaceFileSearchPopup(
4353

4454
private fun loadProjectFiles() {
4555
allProjectFiles.clear()
56+
57+
// Add recent files with higher priority
4658
val fileList = EditorHistoryManager.getInstance(project).fileList
47-
fileList.forEach { file ->
59+
fileList.take(20).forEach { file ->
4860
if (file.canBeAdded(project)) {
4961
val presentation = FilePresentation.from(project, file)
5062
presentation.isRecentFile = true
5163
allProjectFiles.add(presentation)
5264
}
5365
}
5466

67+
// Add all project files
5568
ProjectFileIndex.getInstance(project).iterateContent { file ->
5669
if (file.canBeAdded(project) &&
5770
!ProjectFileIndex.getInstance(project).isUnderIgnored(file) &&
@@ -63,76 +76,125 @@ class WorkspaceFileSearchPopup(
6376
true
6477
}
6578

66-
updateFileList("")
79+
filterFiles("")
6780
}
6881

6982
private fun setupUI() {
70-
searchField.document.addDocumentListener(object : DocumentListener {
71-
override fun insertUpdate(e: DocumentEvent) = updateSearch()
72-
override fun removeUpdate(e: DocumentEvent) = updateSearch()
73-
override fun changedUpdate(e: DocumentEvent) = updateSearch()
74-
75-
private fun updateSearch() {
76-
updateFileList(searchField.text)
83+
// Configure search field
84+
searchField.textEditor.addKeyListener(object : KeyAdapter() {
85+
override fun keyReleased(e: KeyEvent) {
86+
filterFiles(searchField.text.trim())
87+
if (e.keyCode == KeyEvent.VK_DOWN && fileListModel.size > 0) {
88+
fileList.requestFocus()
89+
fileList.selectedIndex = 0
90+
}
91+
}
92+
})
93+
94+
// Configure file list
95+
fileList.addKeyListener(object : KeyAdapter() {
96+
override fun keyPressed(e: KeyEvent) {
97+
when (e.keyCode) {
98+
KeyEvent.VK_ENTER -> {
99+
selectFiles()
100+
e.consume()
101+
}
102+
KeyEvent.VK_ESCAPE -> {
103+
popup?.cancel()
104+
e.consume()
105+
}
106+
KeyEvent.VK_UP -> {
107+
if (fileList.selectedIndex == 0) {
108+
searchField.requestFocus()
109+
e.consume()
110+
}
111+
}
112+
}
77113
}
78114
})
79115

80-
fileList.cellRenderer = FileListCellRenderer()
81116
fileList.addMouseListener(object : MouseAdapter() {
82117
override fun mouseClicked(e: MouseEvent) {
83118
if (e.clickCount == 2) {
84-
val selectedFiles = fileList.selectedValuesList.map { it.virtualFile }
85-
if (selectedFiles.isNotEmpty()) {
86-
onFilesSelected(selectedFiles)
87-
popup?.cancel()
88-
}
119+
selectFiles()
89120
}
90121
}
91122
})
92123

124+
// Setup layout with proper borders and spacing
125+
contentPanel.border = JBUI.Borders.empty()
93126
contentPanel.add(searchField, BorderLayout.NORTH)
94-
contentPanel.add(JScrollPane(fileList), BorderLayout.CENTER)
127+
contentPanel.add(JBScrollPane(fileList), BorderLayout.CENTER)
95128
contentPanel.preferredSize = minPopupSize
96129
}
97130

98-
private fun updateFileList(searchText: String) {
131+
private fun filterFiles(query: String) {
99132
fileListModel.clear()
100-
101-
val filteredFiles = if (searchText.isBlank()) {
102-
allProjectFiles
133+
val filteredFiles = if (query.isBlank()) {
134+
allProjectFiles.take(50) // Show first 50 files when no query
103135
} else {
104-
allProjectFiles.filter { item ->
105-
item.name.contains(searchText, ignoreCase = true) ||
106-
item.path.contains(searchText, ignoreCase = true)
107-
}
136+
allProjectFiles.filter { file ->
137+
file.name.contains(query, ignoreCase = true) ||
138+
file.path.contains(query, ignoreCase = true)
139+
}.take(50)
140+
}
141+
142+
// Sort files: recent files first, then by name
143+
val sortedFiles = filteredFiles.sortedWith(compareBy<FilePresentation> { !it.isRecentFile }.thenBy { it.name })
144+
sortedFiles.forEach { fileListModel.addElement(it) }
145+
146+
// Auto-select first item if available
147+
if (fileListModel.size > 0) {
148+
fileList.selectedIndex = 0
149+
}
150+
}
151+
152+
private fun selectFiles() {
153+
val selectedFiles = fileList.selectedValuesList.map { it.virtualFile }
154+
if (selectedFiles.isNotEmpty()) {
155+
onFilesSelected(selectedFiles)
156+
popup?.cancel()
108157
}
109-
110-
filteredFiles.forEach { fileListModel.addElement(it) }
111158
}
112159

113160
fun show(component: JComponent) {
114161
popup = JBPopupFactory.getInstance()
115-
.createComponentPopupBuilder(contentPanel, searchField)
116-
.setTitle("Search Files")
162+
.createComponentPopupBuilder(contentPanel, searchField.textEditor)
163+
.setTitle("Add Files to Workspace")
117164
.setMovable(true)
118165
.setResizable(true)
119166
.setRequestFocus(true)
120167
.setFocusable(true)
168+
.setCancelOnClickOutside(true)
169+
.setCancelOnOtherWindowOpen(true)
121170
.setMinSize(minPopupSize)
122171
.createPopup()
172+
173+
popup?.addListener(object : JBPopupListener {
174+
override fun onClosed(event: LightweightWindowEvent) {
175+
// Clean up resources when popup is closed
176+
allProjectFiles.clear()
177+
fileListModel.clear()
178+
}
179+
})
123180

124-
val topOffset = (component.border?.getBorderInsets(component)?.top ?: 0)
125-
val leftOffset = (component.border?.getBorderInsets(component)?.left ?: 0)
126-
127-
popup?.show(RelativePoint(component, Point(leftOffset, -minPopupSize.height + topOffset)))
181+
// Show popup in best position
182+
// popup?.showInBestPositionFor(component)
183+
popup?.showInCenterOf(component)
184+
185+
// Request focus for search field after popup is shown
186+
SwingUtilities.invokeLater {
187+
IdeFocusManager.findInstance().requestFocus(searchField.textEditor, false)
188+
}
128189
}
129190

130-
class FileListCellRenderer() : ListCellRenderer<FilePresentation> {
191+
private inner class FileListCellRenderer : ListCellRenderer<FilePresentation> {
131192
private val noBorderFocus = BorderFactory.createEmptyBorder(1, 1, 1, 1)
193+
private val speedSearchComparator = SpeedSearchComparator(false)
132194

133195
@NotNull
134196
override fun getListCellRendererComponent(
135-
list: JList<out FilePresentation>?,
197+
list: JList<out FilePresentation>,
136198
value: FilePresentation,
137199
index: Int,
138200
isSelected: Boolean,
@@ -161,11 +223,11 @@ class WorkspaceFileSearchPopup(
161223
mainPanel.add(infoPanel, BorderLayout.CENTER)
162224

163225
if (isSelected) {
164-
mainPanel.background = list?.selectionBackground
165-
mainPanel.foreground = list?.selectionForeground
226+
mainPanel.background = list.selectionBackground
227+
mainPanel.foreground = list.selectionForeground
166228
} else {
167-
mainPanel.background = list?.background
168-
mainPanel.foreground = list?.foreground
229+
mainPanel.background = list.background
230+
mainPanel.foreground = list.foreground
169231
}
170232

171233
mainPanel.border = if (cellHasFocus) {
@@ -177,4 +239,4 @@ class WorkspaceFileSearchPopup(
177239
return mainPanel
178240
}
179241
}
180-
}
242+
}

0 commit comments

Comments
 (0)