Skip to content

Commit a88627e

Browse files
committed
Split @ command search into instant + heavy phases so builtin actions (@includeopenfiles) appear immediately while file/folder/git scanning runs in parallel in the background.
Also passes actual search text to group lookups instead of empty string, adds early termination (200 file cap) to prevent full project tree scans, and deduplicates merged results.
1 parent 27db40a commit a88627e

File tree

3 files changed

+96
-22
lines changed

3 files changed

+96
-22
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -636,13 +636,24 @@ class PromptTextField(
636636
}
637637

638638
private suspend fun updateLookupWithSearchResults(searchText: String) {
639-
val matchedResults = searchManager.performGlobalSearch(searchText)
639+
// Phase 1: Show instant results (builtins, history, personas, MCP) immediately
640+
val instantResults = searchManager.performInstantSearch(searchText)
641+
if (instantResults.isNotEmpty()) {
642+
withContext(Dispatchers.Main) {
643+
showGlobalSearchResults(instantResults, searchText)
644+
}
645+
}
640646

641-
if (lastSearchResults != matchedResults) {
642-
lastSearchResults = matchedResults
647+
// Phase 2: Get heavy results (files, folders, git) and merge
648+
val heavyResults = searchManager.performHeavySearch(searchText)
649+
if (heavyResults.isNotEmpty()) {
650+
val allResults = searchManager.mergeResults(instantResults, heavyResults, searchText)
651+
lastSearchResults = allResults
643652
withContext(Dispatchers.Main) {
644-
showGlobalSearchResults(matchedResults, searchText)
653+
showGlobalSearchResults(allResults, searchText)
645654
}
655+
} else {
656+
lastSearchResults = instantResults
646657
}
647658
}
648659

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

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
1111
import ee.carlrobert.codegpt.ui.textarea.lookup.action.DiagnosticsActionItem
1212
import ee.carlrobert.codegpt.ui.textarea.lookup.action.ImageActionItem
1313
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
14+
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.IncludeOpenFilesActionItem
15+
import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.IncludeCurrentChangesActionItem
1416
import ee.carlrobert.codegpt.ui.textarea.lookup.group.*
1517
import kotlinx.coroutines.CancellationException
18+
import kotlinx.coroutines.async
19+
import kotlinx.coroutines.coroutineScope
1620

1721
data class SearchState(
1822
val isInSearchContext: Boolean = false,
@@ -63,33 +67,83 @@ class SearchManager(
6367
ImageActionItem(project, tagManager)
6468
).filter { it.enabled }
6569

66-
suspend fun performGlobalSearch(searchText: String): List<LookupActionItem> {
67-
val allGroups =
68-
getDefaultGroups().filterNot { it is WebActionItem || it is ImageActionItem }
69-
val allResults = mutableListOf<LookupActionItem>()
70+
suspend fun performInstantSearch(searchText: String): List<LookupActionItem> {
71+
val groups = getDefaultGroups()
72+
val results = mutableListOf<LookupActionItem>()
73+
74+
// Standalone action items that are normally buried inside heavy groups
75+
if (groups.any { it is FilesGroupItem }) {
76+
results.add(IncludeOpenFilesActionItem())
77+
}
78+
if (GitFeatureAvailability.isAvailable && groups.any { it is GitGroupItem }) {
79+
results.add(IncludeCurrentChangesActionItem())
80+
}
7081

71-
allGroups.forEach { group ->
82+
// Lightweight groups (in-memory data only)
83+
val lightGroups = groups
84+
.filterNot { it is FilesGroupItem || it is FoldersGroupItem || it is GitGroupItem }
85+
.filterNot { it is WebActionItem || it is ImageActionItem }
86+
87+
lightGroups.forEach { group ->
7288
try {
7389
if (group is LookupGroupItem) {
74-
val lookupActionItems =
75-
group.getLookupItems("").filterIsInstance<LookupActionItem>()
76-
allResults.addAll(lookupActionItems)
90+
results.addAll(
91+
group.getLookupItems(searchText).filterIsInstance<LookupActionItem>()
92+
)
7793
}
7894
} catch (e: CancellationException) {
7995
throw e
8096
} catch (e: Exception) {
81-
logger.error("Error getting results from ${group::class.simpleName}", e)
97+
logger.error("Error getting instant results from ${group::class.simpleName}", e)
8298
}
8399
}
84100

85101
if (featureType != FeatureType.INLINE_EDIT && featureType != FeatureType.AGENT) {
86102
val webAction = WebActionItem(tagManager)
87103
if (webAction.enabled()) {
88-
allResults.add(webAction)
104+
results.add(webAction)
89105
}
90106
}
91107

92-
return filterAndSortResults(allResults, searchText)
108+
return filterAndSortResults(results, searchText)
109+
}
110+
111+
suspend fun performHeavySearch(searchText: String): List<LookupActionItem> = coroutineScope {
112+
val heavyGroups = getDefaultGroups()
113+
.filter { it is FilesGroupItem || it is FoldersGroupItem || it is GitGroupItem }
114+
115+
heavyGroups.map { group ->
116+
async {
117+
try {
118+
if (group is LookupGroupItem) {
119+
group.getLookupItems(searchText).filterIsInstance<LookupActionItem>()
120+
} else {
121+
emptyList()
122+
}
123+
} catch (e: CancellationException) {
124+
throw e
125+
} catch (e: Exception) {
126+
logger.error("Error getting results from ${group::class.simpleName}", e)
127+
emptyList()
128+
}
129+
}
130+
}.flatMap { it.await() }
131+
}
132+
133+
fun mergeResults(
134+
instantResults: List<LookupActionItem>,
135+
heavyResults: List<LookupActionItem>,
136+
searchText: String
137+
): List<LookupActionItem> {
138+
val seenNames = instantResults.map { it.displayName }.toMutableSet()
139+
val dedupedHeavy = heavyResults.filter { seenNames.add(it.displayName) }
140+
return filterAndSortResults(instantResults + dedupedHeavy, searchText)
141+
}
142+
143+
suspend fun performGlobalSearch(searchText: String): List<LookupActionItem> {
144+
val instant = performInstantSearch(searchText)
145+
val heavy = performHeavySearch(searchText)
146+
return mergeResults(instant, heavy, searchText)
93147
}
94148

95149
private fun filterAndSortResults(

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,21 @@ class FilesGroupItem(
5151
val projectFileIndex = project.service<ProjectFileIndex>()
5252
val matcher = NameUtil.buildMatcher("*$searchText").build()
5353
val matchingFiles = mutableListOf<VirtualFile>()
54+
val maxFiles = if (searchText.isNotEmpty()) MAX_SEARCH_FILES else Int.MAX_VALUE
5455

5556
projectFileIndex.iterateContent { file ->
56-
if (!file.isDirectory &&
57-
settingsService.isVirtualFileVisible(file) &&
58-
!containsTag(file) &&
59-
(searchText.isEmpty() || matcher.matchingDegree(file.name) != Int.MIN_VALUE)
60-
) {
61-
matchingFiles.add(file)
57+
if (matchingFiles.size >= maxFiles) {
58+
false
59+
} else {
60+
if (!file.isDirectory &&
61+
settingsService.isVirtualFileVisible(file) &&
62+
!containsTag(file) &&
63+
(searchText.isEmpty() || matcher.matchingDegree(file.name) != Int.MIN_VALUE)
64+
) {
65+
matchingFiles.add(file)
66+
}
67+
true
6268
}
63-
true
6469
}
6570

6671
val openFiles = project.service<FileEditorManager>().openFiles
@@ -75,6 +80,10 @@ class FilesGroupItem(
7580
}
7681
}
7782

83+
companion object {
84+
private const val MAX_SEARCH_FILES = 200
85+
}
86+
7887
private fun containsTag(file: VirtualFile): Boolean {
7988
return tagManager.containsTag(file)
8089
}

0 commit comments

Comments
 (0)