Skip to content

Commit 6ae7b6e

Browse files
author
a.iudin
committed
Refactor dynamic lookup groups: optimize file/folder suggestion, unify tag filtering, and introduce loading state
1 parent c997c95 commit 6ae7b6e

File tree

7 files changed

+228
-61
lines changed

7 files changed

+228
-61
lines changed

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ import com.intellij.util.ui.JBUI
3030
import ee.carlrobert.codegpt.CodeGPTBundle
3131
import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT
3232
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
33-
import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem
34-
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
35-
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
36-
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem
33+
import ee.carlrobert.codegpt.ui.textarea.lookup.*
3734
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
3835
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
3936
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
@@ -66,6 +63,7 @@ class PromptTextField(
6663
private val coroutineScope = CoroutineScope(Dispatchers.EDT + SupervisorJob())
6764
private var showSuggestionsJob: Job? = null
6865
private var allGroupItemsJob: Job? = null
66+
private var searchJob: Job? = null
6967

7068
val dispatcherId: UUID = UUID.randomUUID()
7169
private var currentPopup: JBPopup? = null
@@ -300,32 +298,68 @@ class PromptTextField(
300298
fun updateFilter() {
301299
val searchText = searchField.text
302300

303-
showSuggestionsJob?.cancel()
304-
showSuggestionsJob = coroutineScope.launch {
301+
searchJob?.cancel()
302+
303+
if (searchText.isEmpty()) {
304+
updateListItems(currentItems)
305+
return
306+
}
307+
308+
searchJob = coroutineScope.launch {
305309
if (parentGroup != null && searchText.length >= 2) {
306-
if (parentGroup is DynamicLookupGroupItem) {
307-
val items = parentGroup.updateLookupItems(searchText)
308-
updateListItems(items)
309-
} else {
310-
val items = parentGroup.getLookupItems(searchText)
311-
updateListItems(items)
310+
showLoadingState()
311+
312+
runCatchingCancellable {
313+
val items = if (parentGroup is DynamicLookupGroupItem) {
314+
parentGroup.updateLookupItems(searchText)
315+
} else {
316+
parentGroup.getLookupItems(searchText)
317+
}
318+
.distinctBy { it.displayName }
319+
320+
if (isActive) {
321+
updateListItems(items)
322+
}
312323
}
313-
} else {
314-
if (searchText.length < 2) {
315-
val filteredItems = currentItems.filter {
316-
searchText.isEmpty() || it.displayName.contains(searchText, ignoreCase = true)
324+
.onFailure {
325+
updateListItems(emptyList())
317326
}
327+
} else if (searchText.length >= 2) {
328+
showLoadingState()
329+
runCatchingCancellable {
330+
val filteredItems = currentItems
331+
.flatMap { item ->
332+
when (item) {
333+
is DynamicLookupGroupItem -> item.getLookupItems(searchText).take(5)
334+
is LookupGroupItem -> item.getLookupItems(searchText).take(5)
335+
else -> listOf(item)
336+
}
337+
}
338+
.distinctBy { it.displayName }
339+
.filter { it.displayName.contains(searchText, ignoreCase = true) }
318340
updateListItems(filteredItems)
319-
} else {
320-
val filteredItems = allAvailableItems.filter {
321-
it.displayName.contains(searchText, ignoreCase = true)
341+
}
342+
.onFailure {
343+
updateListItems(emptyList())
322344
}
323-
updateListItems(filteredItems)
345+
} else {
346+
val filteredItems = currentItems.filter {
347+
it.displayName.contains(searchText, ignoreCase = true)
324348
}
349+
updateListItems(filteredItems)
325350
}
326351
}
327352
}
328353

354+
private fun showLoadingState() {
355+
runInEdt {
356+
val loadingItems = listOf(LoadingLookupItem())
357+
val model = LookupListModel(loadingItems)
358+
itemsList.model = model
359+
itemsList.selectedIndex = -1
360+
}
361+
}
362+
329363
private fun updateListItems(items: List<LookupItem>) {
330364
runInEdt {
331365
val model = LookupListModel(items)
@@ -340,6 +374,11 @@ class PromptTextField(
340374
val selectedIndex = itemsList.selectedIndex
341375
if (selectedIndex >= 0) {
342376
val selectedItem = (itemsList.model as LookupListModel).getElementAt(selectedIndex)
377+
378+
if (selectedItem is LoadingLookupItem) {
379+
return
380+
}
381+
343382
editor?.let { handleItemSelection(it, selectedItem) }
344383
currentPopup?.cancel()
345384
}
@@ -468,6 +507,7 @@ class PromptTextField(
468507
}
469508

470509
override fun dispose() {
510+
searchJob?.cancel()
471511
showSuggestionsJob?.cancel()
472512
allGroupItemsJob?.cancel()
473513
currentPopup?.cancel()

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ class UserInputHeaderPanel(
258258
private inner class FileSelectionListener : FileEditorManagerListener {
259259
override fun selectionChanged(event: FileEditorManagerEvent) {
260260
event.newFile?.let { newFile ->
261-
val containsTag = tagManager.getTags()
262-
.none { it is EditorTagDetails && it.virtualFile == newFile }
261+
val tags = tagManager.getTags()
262+
val containsTag = !tags.contains(EditorTagDetails(newFile))
263263
if (containsTag) {
264264
tagManager.addTag(EditorTagDetails(newFile).apply { selected = false })
265265
}

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagManager.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.ui.textarea.header.tag
33
import com.intellij.openapi.Disposable
44
import com.intellij.openapi.application.ApplicationManager
55
import com.intellij.openapi.components.service
6-
import com.intellij.openapi.vfs.VirtualFile
76
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
87
import ee.carlrobert.codegpt.settings.configuration.ConfigurationStateListener
98
import java.util.concurrent.CopyOnWriteArraySet
@@ -36,21 +35,6 @@ class TagManager(parentDisposable: Disposable) {
3635

3736
fun getTags(): Set<TagDetails> = synchronized(this) { tags.toSet() }
3837

39-
fun containsTag(file: VirtualFile): Boolean = tags.any {
40-
// TODO: refactor
41-
if (it is SelectionTagDetails) {
42-
it.virtualFile == file
43-
} else if (it is FileTagDetails) {
44-
it.virtualFile == file
45-
} else if (it is EditorSelectionTagDetails) {
46-
it.virtualFile == file
47-
} else if (it is EditorTagDetails) {
48-
it.virtualFile == file
49-
} else {
50-
false
51-
}
52-
}
53-
5438
fun addTag(tagDetails: TagDetails) {
5539
val wasAdded = synchronized(this) {
5640
if (!service<ConfigurationSettings>().state.chatCompletionSettings.editorContextTagEnabled
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ee.carlrobert.codegpt.ui.textarea.lookup
2+
3+
import com.intellij.codeInsight.lookup.LookupElement
4+
import com.intellij.codeInsight.lookup.LookupElementPresentation
5+
import com.intellij.icons.AllIcons
6+
import ee.carlrobert.codegpt.CodeGPTBundle
7+
import javax.swing.Icon
8+
9+
class LoadingLookupItem : AbstractLookupItem() {
10+
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.loading.displayName")
11+
override val icon: Icon = AllIcons.Process.Step_1
12+
13+
override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) {
14+
presentation.icon = icon
15+
presentation.itemText = displayName
16+
presentation.isItemTextBold = false
17+
presentation.isItemTextItalic = true
18+
presentation.itemTextForeground = com.intellij.ui.JBColor.GRAY
19+
}
20+
21+
override fun getLookupString(): String {
22+
return "loading_search"
23+
}
24+
}

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import com.intellij.openapi.fileEditor.FileEditorManager
77
import com.intellij.openapi.project.Project
88
import com.intellij.openapi.roots.ProjectFileIndex
99
import com.intellij.openapi.vfs.VirtualFile
10+
import com.intellij.openapi.vfs.isFile
1011
import ee.carlrobert.codegpt.CodeGPTBundle
1112
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
1213
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
13-
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagUtil
1414
import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem
1515
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
1616
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem
@@ -28,36 +28,149 @@ class FilesGroupItem(
2828
override val icon = AllIcons.FileTypes.Any_type
2929

3030
override suspend fun updateLookupItems(searchText: String): List<LookupItem> {
31+
return getFileItems(searchText)
32+
}
33+
34+
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
35+
return getFileItems(searchText)
36+
}
37+
38+
private suspend fun getFileItems(searchText: String): List<LookupActionItem> {
3139
return withContext(Dispatchers.IO) {
32-
val items = mutableListOf<LookupItem>()
33-
project.service<ProjectFileIndex>().iterateContent {
34-
if (!it.isDirectory && !containsTag(it) &&
35-
(searchText.isEmpty() || it.name.contains(searchText, ignoreCase = true))) {
36-
items.add(FileActionItem(project, it))
40+
val fileEditorManager = project.service<FileEditorManager>()
41+
val projectFileIndex = project.service<ProjectFileIndex>()
42+
43+
val (activeFiles, otherOpenFiles) = readAction {
44+
val selectedFiles = fileEditorManager.selectedFiles.toList()
45+
val openFiles = fileEditorManager.openFiles.toList()
46+
val otherFiles = openFiles.filterNot { it in selectedFiles }
47+
48+
Pair(selectedFiles, otherFiles)
49+
}
50+
51+
val filteredActiveFiles = activeFiles.filter { isValidFile(it, searchText, projectFileIndex) }
52+
val filteredOpenFiles = otherOpenFiles.filter { isValidFile(it, searchText, projectFileIndex) }
53+
54+
val editorFilesCount = filteredActiveFiles.size + filteredOpenFiles.size
55+
val needFromFileSystem = maxOf(0, 30 - editorFilesCount)
56+
57+
val filesFromSystem = mutableListOf<VirtualFile>()
58+
if (needFromFileSystem > 0) {
59+
val editorFilesSet = (filteredActiveFiles + filteredOpenFiles).toSet()
60+
61+
readAction {
62+
projectFileIndex.iterateContent(
63+
/* processor = */ { file ->
64+
if (filesFromSystem.size >= needFromFileSystem) {
65+
false
66+
} else {
67+
if (!editorFilesSet.contains(file)) {
68+
filesFromSystem.add(file)
69+
}
70+
true
71+
}
72+
},
73+
/* filter = */ { file ->
74+
!file.isDirectory &&
75+
isValidProjectFile(file, projectFileIndex) &&
76+
!containsTag(file) &&
77+
(searchText.isEmpty() || file.name.contains(searchText, ignoreCase = true))
78+
}
79+
)
3780
}
38-
items.size < 50
3981
}
40-
items
82+
83+
val allFiles = filteredActiveFiles + filteredOpenFiles + filesFromSystem
84+
85+
val result = allFiles
86+
.map { FileActionItem(project, it) }
87+
.toMutableList<LookupActionItem>()
88+
89+
if (searchText.isEmpty()) {
90+
result.add(IncludeOpenFilesActionItem())
91+
}
92+
93+
result.toList()
4194
}
4295
}
4396

44-
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
45-
return readAction {
46-
val projectFileIndex = project.service<ProjectFileIndex>()
47-
project.service<FileEditorManager>().openFiles
48-
.filter { projectFileIndex.isInContent(it) && !containsTag(it) }
49-
.toFileSuggestions()
50-
}
97+
private fun isValidProjectFile(file: VirtualFile, projectFileIndex: ProjectFileIndex): Boolean {
98+
return file.isFile &&
99+
!isExcludedFile(file) &&
100+
projectFileIndex.isInContent(file) &&
101+
!projectFileIndex.isInLibraryClasses(file) &&
102+
!projectFileIndex.isInLibrarySource(file) &&
103+
!projectFileIndex.isInGeneratedSources(file)
104+
}
105+
106+
private fun isExcludedFile(file: VirtualFile): Boolean {
107+
return file.extension?.lowercase() in EXCLUDED_EXTENSIONS
108+
}
109+
110+
private fun isValidFile(file: VirtualFile, searchText: String, projectFileIndex: ProjectFileIndex): Boolean {
111+
return isValidProjectFile(file, projectFileIndex) &&
112+
!containsTag(file) &&
113+
(searchText.isEmpty() || file.name.contains(searchText, ignoreCase = true))
51114
}
52115

53116
private fun containsTag(file: VirtualFile): Boolean {
54-
return tagManager.containsTag(file)
117+
val tags = tagManager.getTags()
118+
return tags.contains(FileTagDetails(file))
55119
}
56120

57-
private fun Iterable<VirtualFile>.toFileSuggestions(): List<LookupActionItem> {
58-
val selectedFileTags = TagUtil.getExistingTags(project, FileTagDetails::class.java)
59-
return filter { file -> selectedFileTags.none { it.virtualFile == file } }
60-
.take(10)
61-
.map { FileActionItem(project, it) } + listOf(IncludeOpenFilesActionItem())
121+
private companion object {
122+
val COMPILED_EXTENSIONS = setOf(
123+
// Java/JVM languages
124+
"class", "jar", "war", "ear", "aar",
125+
126+
// C/C++/Objective-C
127+
"o", "obj", "so", "dll", "dylib", "a", "lib", "framework",
128+
129+
// .NET/C#
130+
"exe", "pdb", "mdb",
131+
132+
// Python
133+
"pyc", "pyo", "pyd",
134+
135+
// Rust
136+
"rlib",
137+
138+
// Go (compiled binaries often have no extension, but some cases)
139+
"a",
140+
141+
// Android
142+
"dex", "apk",
143+
144+
// iOS
145+
"ipa",
146+
147+
// Pascal/Delphi
148+
"dcu", "dcp",
149+
150+
// PHP
151+
"phar",
152+
153+
// Archives and packages
154+
"zip", "tar", "gz", "bz2", "xz", "7z", "rar",
155+
156+
// Other binary formats
157+
"bin", "dat", "dump"
158+
)
159+
160+
val TEMPORARY_EXTENSIONS = setOf(
161+
// Backup files
162+
"bak", "backup", "tmp", "temp", "swp", "swo",
163+
164+
// IDE/Editor temporary files
165+
"idea", "iml", "ipr", "iws",
166+
167+
// OS temporary files
168+
"ds_store", "thumbs.db", "desktop.ini",
169+
170+
// Build artifacts
171+
"log", "cache"
172+
)
173+
174+
val EXCLUDED_EXTENSIONS = COMPILED_EXTENSIONS + TEMPORARY_EXTENSIONS
62175
}
63176
}

0 commit comments

Comments
 (0)