feat: global context search

This commit is contained in:
Carl-Robert Linnupuu 2025-06-06 00:54:26 +01:00
parent c3890870c4
commit e0dbb030fb
5 changed files with 614 additions and 154 deletions

View file

@ -1,27 +1,23 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.codeInsight.lookup.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupEvent
import com.intellij.codeInsight.lookup.LookupListener
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.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.markup.HighlighterLayer
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.wm.ToolWindowManager
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT
@ -29,27 +25,30 @@ 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.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 kotlinx.coroutines.*
import java.awt.Dimension
import java.util.*
class PromptTextField(
private val project: Project,
private val tagManager: TagManager,
tagManager: TagManager,
private val onTextChanged: (String) -> Unit,
private val onBackSpace: () -> Unit,
private val onLookupAdded: (LookupActionItem) -> Unit,
private val onSubmit: (String) -> Unit,
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
companion object {
private val logger = thisLogger()
}
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val lookupManager = PromptTextFieldLookupManager(project, onLookupAdded)
private val searchManager = SearchManager(project, tagManager)
private var showSuggestionsJob: Job? = null
private var searchState = SearchState()
private var lastSearchResults: List<LookupActionItem>? = null
val dispatcherId: UUID = UUID.randomUUID()
var lookup: LookupImpl? = null
@ -62,14 +61,14 @@ class PromptTextField(
override fun onEditorAdded(editor: Editor) {
IdeEventQueue.getInstance().addDispatcher(
PromptTextFieldEventDispatcher(dispatcherId, onBackSpace, lookup) {
PromptTextFieldEventDispatcher(dispatcherId, onBackSpace, lookup) { event ->
val shown = lookup?.let { it.isShown && !it.isLookupDisposed } == true
if (shown) {
return@PromptTextFieldEventDispatcher
}
onSubmit(text)
it.consume()
event.consume()
},
this
)
@ -82,63 +81,48 @@ class PromptTextField(
}
suspend fun showGroupLookup() {
val lookupItems = listOf(
FilesGroupItem(project, tagManager),
FoldersGroupItem(project, tagManager),
GitGroupItem(project),
PersonasGroupItem(tagManager),
DocsGroupItem(tagManager),
MCPGroupItem(),
WebActionItem(tagManager)
)
.filter { it.enabled }
val lookupItems = searchManager.getDefaultGroups()
.map { it.createLookupElement() }
.toTypedArray()
withContext(Dispatchers.Main) {
editor?.let {
showGroupLookup(it, lookupItems)
editor?.let { editor ->
lookup = lookupManager.showGroupLookup(
editor = editor,
lookupElements = lookupItems,
onGroupSelected = { group, selectedText ->
handleGroupSelected(group, selectedText)
},
onWebActionSelected = { webAction ->
onLookupAdded(webAction)
}
)
}
}
}
private fun showGroupLookup(editor: Editor, lookupElements: Array<LookupElement>) {
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
val offset = editor.caretModel.offset
val start = offset - lookupString.length
if (start >= 0) {
runUndoTransparentWriteAction {
editor.document.deleteString(start, offset)
}
}
if (suggestion is WebActionItem) {
onLookupAdded(suggestion)
}
if (suggestion !is LookupGroupItem) return
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
showGroupSuggestions(suggestion)
}
private fun showGlobalSearchResults(
results: List<LookupActionItem>,
searchText: String
) {
editor?.let { editor ->
try {
hideLookupIfShown()
lookup = lookupManager.showSearchResultsLookup(editor, results, searchText)
} catch (e: Exception) {
logger.error("Error showing lookup: $e", e)
}
})
lookup?.refreshUi(false, true)
lookup?.showLookup()
}
}
private fun findAtSymbolPosition(editor: Editor): Int {
val atPos = editor.document.text.lastIndexOf('@')
return if (atPos >= 0) atPos else -1
private fun handleGroupSelected(group: LookupGroupItem, searchText: String) {
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
showGroupSuggestions(group, searchText)
}
}
private suspend fun showGroupSuggestions(group: LookupGroupItem) {
private suspend fun showGroupSuggestions(group: LookupGroupItem, filterText: String = "") {
val suggestions = group.getLookupItems()
if (suggestions.isEmpty()) {
return
@ -147,99 +131,53 @@ class PromptTextField(
val lookupElements = suggestions.map { it.createLookupElement() }.toTypedArray()
withContext(Dispatchers.Main) {
showSuggestionLookup(lookupElements, group)
showSuggestionLookup(lookupElements, group, filterText)
}
}
private fun createLookup(
editor: Editor,
lookupElements: Array<LookupElement>,
searchText: String
) = runReadAction {
LookupManager.getInstance(project).createLookup(
editor,
lookupElements,
searchText,
LookupArranger.DefaultArranger()
) as LookupImpl
}
private fun showSuggestionLookup(
lookupElements: Array<LookupElement>,
parentGroup: LookupGroupItem,
filterText: String = "",
) {
editor?.let {
lookup = createLookup(it, lookupElements, filterText)
editor?.let { editor ->
searchState = searchState.copy(isInGroupLookupContext = true)
lookup = lookupManager.showSuggestionLookup(
editor = editor,
lookupElements = lookupElements,
parentGroup = parentGroup,
onDynamicUpdate = { searchText ->
handleDynamicUpdate(parentGroup, lookupElements, searchText, filterText)
},
filterText = 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 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)
}
}
}
override fun lookupCanceled(event: LookupEvent) {
searchState = searchState.copy(isInGroupLookupContext = false)
}
})
}
}
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)
}
}
private fun handleDynamicUpdate(
parentGroup: LookupGroupItem,
lookupElements: Array<LookupElement>,
searchText: String,
filterText: String
) {
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
if (parentGroup is DynamicLookupGroupItem) {
if (searchText.length >= PromptTextFieldConstants.MIN_DYNAMIC_SEARCH_LENGTH) {
parentGroup.updateLookupList(lookup!!, searchText)
} else if (searchText.isEmpty()) {
withContext(Dispatchers.Main) {
showSuggestionLookup(lookupElements, parentGroup, filterText)
}
}
override fun afterTruncate() {
if (parentGroup is DynamicLookupGroupItem) {
val searchText = getSearchText()
if (searchText.isEmpty()) {
showSuggestionLookup(lookupElements, parentGroup, filterText)
}
}
}
private fun getSearchText(): String {
val text = it.document.text
return text.substring(text.lastIndexOf("@") + 1)
}
}, this)
lookup?.refreshUi(false, true)
lookup?.showLookup()
}
}
}
@ -252,11 +190,17 @@ class PromptTextField(
}
override fun updateBorder(editor: EditorEx) {
editor.setBorder(JBUI.Borders.empty(4, 8))
editor.setBorder(
JBUI.Borders.empty(
PromptTextFieldConstants.BORDER_PADDING,
PromptTextFieldConstants.BORDER_SIDE_PADDING
)
)
}
override fun dispose() {
showSuggestionsJob?.cancel()
lastSearchResults = null
}
private fun setupDocumentListener(editor: EditorEx) {
@ -264,19 +208,140 @@ class PromptTextField(
override fun documentChanged(event: DocumentEvent) {
adjustHeight(editor)
onTextChanged(event.document.text)
if ("@" == event.newFragment.toString()) {
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
showGroupLookup()
}
}
handleDocumentChange(event)
}
}, this)
}
private fun handleDocumentChange(event: DocumentEvent) {
val text = event.document.text
val caretOffset = event.offset + event.newLength
when {
isAtSymbolTyped(event) -> handleAtSymbolTyped()
else -> handleTextChange(text, caretOffset)
}
}
private fun isAtSymbolTyped(event: DocumentEvent): Boolean {
return PromptTextFieldConstants.AT_SYMBOL == event.newFragment.toString()
}
private fun handleAtSymbolTyped() {
searchState = searchState.copy(
isInSearchContext = true,
lastSearchText = ""
)
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
showGroupLookup()
}
}
private fun handleTextChange(text: String, caretOffset: Int) {
val searchText = searchManager.getSearchTextAfterAt(text, caretOffset)
when {
searchText != null && searchText.isEmpty() -> handleEmptySearch()
!searchText.isNullOrEmpty() -> handleNonEmptySearch(searchText)
searchText == null -> handleNoSearch()
}
}
private fun handleEmptySearch() {
if (!searchState.isInSearchContext || searchState.lastSearchText != "") {
searchState = searchState.copy(
isInSearchContext = true,
lastSearchText = "",
isInGroupLookupContext = false
)
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
updateLookupWithGroups()
}
}
}
private fun handleNonEmptySearch(searchText: String) {
if (!searchState.isInGroupLookupContext) {
if (!searchManager.matchesAnyDefaultGroup(searchText)) {
if (!searchState.isInSearchContext || searchState.lastSearchText != searchText) {
searchState = searchState.copy(
isInSearchContext = true,
lastSearchText = searchText
)
showSuggestionsJob?.cancel()
showSuggestionsJob = coroutineScope.launch {
delay(PromptTextFieldConstants.SEARCH_DELAY_MS)
updateLookupWithSearchResults(searchText)
}
}
}
}
}
private fun handleNoSearch() {
if (searchState.isInSearchContext) {
searchState = SearchState()
showSuggestionsJob?.cancel()
hideLookupIfShown()
}
}
private fun hideLookupIfShown() {
lookup?.let { existingLookup ->
if (!existingLookup.isLookupDisposed && existingLookup.isShown) {
runInEdt { existingLookup.hide() }
}
}
}
private suspend fun updateLookupWithGroups() {
val lookupItems = searchManager.getDefaultGroups()
.map { it.createLookupElement() }
.toTypedArray()
withContext(Dispatchers.Main) {
editor?.let { editor ->
lookup?.let { existingLookup ->
if (existingLookup.isShown && !existingLookup.isLookupDisposed) {
existingLookup.hide()
}
}
lookup = lookupManager.showGroupLookup(
editor = editor,
lookupElements = lookupItems,
onGroupSelected = { group, currentSearchText ->
handleGroupSelected(
group,
currentSearchText
)
},
onWebActionSelected = { webAction -> onLookupAdded(webAction) },
searchText = ""
)
}
}
}
private suspend fun updateLookupWithSearchResults(searchText: String) {
val matchedResults = searchManager.performGlobalSearch(searchText)
if (lastSearchResults != matchedResults) {
lastSearchResults = matchedResults
withContext(Dispatchers.Main) {
showGlobalSearchResults(matchedResults, searchText)
}
}
}
private fun adjustHeight(editor: EditorEx) {
val contentHeight = editor.contentComponent.preferredSize.height + 8
val contentHeight =
editor.contentComponent.preferredSize.height + PromptTextFieldConstants.HEIGHT_PADDING
val maxHeight = JBUI.scale(getToolWindowHeight() / 2)
val newHeight = minOf(contentHeight, maxHeight)
@ -289,6 +354,7 @@ class PromptTextField(
private fun getToolWindowHeight(): Int {
return project.service<ToolWindowManager>()
.getToolWindow("ProxyAI")?.component?.visibleRect?.height ?: 400
.getToolWindow("ProxyAI")?.component?.visibleRect?.height
?: PromptTextFieldConstants.DEFAULT_TOOL_WINDOW_HEIGHT
}
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.codegpt.ui.textarea
object PromptTextFieldConstants {
const val SEARCH_DELAY_MS = 200L
const val MIN_DYNAMIC_SEARCH_LENGTH = 2
const val MAX_SEARCH_RESULTS = 100
const val DEFAULT_TOOL_WINDOW_HEIGHT = 400
const val BORDER_PADDING = 4
const val BORDER_SIDE_PADDING = 8
const val HEIGHT_PADDING = 8
val DEFAULT_GROUP_NAMES = listOf(
"files", "file", "f",
"folders", "folder", "fold",
"git", "g",
"personas", "persona", "p",
"docs", "doc", "d",
"mcp", "m",
"web", "w"
)
const val AT_SYMBOL = "@"
const val SPACE = " "
const val NEWLINE = "\n"
const val LIGHT_THEME_COLOR = 0x00627A
const val DARK_THEME_COLOR = 0xCC7832
}

View file

@ -0,0 +1,238 @@
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.openapi.application.runReadAction
import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.project.Project
import com.intellij.ui.JBColor
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.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
class PromptTextFieldLookupManager(
private val project: Project,
private val onLookupAdded: (LookupActionItem) -> Unit
) {
fun createLookup(
editor: Editor,
lookupElements: Array<LookupElement>,
searchText: String
): LookupImpl = runReadAction {
LookupManager.getInstance(project).createLookup(
editor,
lookupElements,
searchText,
LookupArranger.DefaultArranger()
) as LookupImpl
}
fun showGroupLookup(
editor: Editor,
lookupElements: Array<LookupElement>,
onGroupSelected: (group: LookupGroupItem, searchText: String) -> Unit,
onWebActionSelected: (WebActionItem) -> Unit,
searchText: String = ""
): LookupImpl {
val lookup = createLookup(editor, lookupElements, "")
lookup.addLookupListener(object : LookupListener {
override fun itemSelected(event: LookupEvent) {
val suggestion = event.item?.getUserData(LookupItem.KEY) ?: return
replaceAtSymbol(editor, suggestion)
when (suggestion) {
is WebActionItem -> onWebActionSelected(suggestion)
is LookupGroupItem -> onGroupSelected(suggestion, searchText)
}
}
})
lookup.refreshUi(false, true)
lookup.showLookup()
return lookup
}
fun showSearchResultsLookup(
editor: Editor,
results: List<LookupActionItem>,
searchText: String
): LookupImpl {
val lookupElements = results.map { it.createLookupElement() }.toTypedArray()
val 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) as? LookupActionItem ?: return
val offset = editor.caretModel.offset
val start = offset - lookupString.length
if (start >= 0) {
runUndoTransparentWriteAction {
editor.document.deleteString(start, offset)
}
}
replaceAtSymbolWithSearch(editor, suggestion, searchText)
onLookupAdded(suggestion)
}
})
lookup.refreshUi(false, true)
lookup.showLookup()
return lookup
}
fun showSuggestionLookup(
editor: Editor,
lookupElements: Array<LookupElement>,
parentGroup: LookupGroupItem,
onDynamicUpdate: (String) -> Unit,
filterText: String = ""
): LookupImpl {
val lookup = createLookup(editor, lookupElements, filterText)
lookup.addLookupListener(object : LookupListener {
override fun itemSelected(event: LookupEvent) {
val lookupString = event.item?.lookupString ?: return
val suggestion =
event.item?.getUserData(LookupItem.KEY) as? LookupActionItem ?: return
val offset = editor.caretModel.offset
val start = offset - lookupString.length
if (start >= 0) {
runUndoTransparentWriteAction {
editor.document.deleteString(start, offset)
}
}
replaceAtSymbolWithSearch(editor, suggestion, filterText)
onLookupAdded(suggestion)
}
})
if (parentGroup is DynamicLookupGroupItem) {
setupDynamicLookupListener(lookup, onDynamicUpdate)
}
lookup.refreshUi(false, true)
lookup.showLookup()
return lookup
}
private fun setupDynamicLookupListener(
lookup: LookupImpl,
onDynamicUpdate: (String) -> Unit
) {
lookup.addPrefixChangeListener(object : PrefixChangeListener {
override fun afterAppend(c: Char) {
val searchText = getSearchTextFromLookup(lookup)
if (searchText.length >= PromptTextFieldConstants.MIN_DYNAMIC_SEARCH_LENGTH) {
onDynamicUpdate(searchText)
}
}
override fun afterTruncate() {
val searchText = getSearchTextFromLookup(lookup)
if (searchText.isEmpty()) {
onDynamicUpdate("")
}
}
}, lookup)
}
private fun getSearchTextFromLookup(lookup: LookupImpl): String {
val editor = lookup.editor
val text = editor.document.text
val atIndex = text.lastIndexOf(PromptTextFieldConstants.AT_SYMBOL)
return if (atIndex >= 0) text.substring(atIndex + 1) else ""
}
private fun getSearchTextFromEditor(editor: Editor): String {
val text = editor.document.text
val caretOffset = editor.caretModel.offset
val atIndex = text.lastIndexOf(PromptTextFieldConstants.AT_SYMBOL)
return if (atIndex >= 0 && atIndex < caretOffset) {
text.substring(atIndex + 1, caretOffset)
} else {
""
}
}
private fun replaceAtSymbolWithSearch(
editor: Editor,
lookupItem: LookupItem,
searchText: String
) {
val atPos = findAtSymbolPosition(editor)
if (atPos >= 0) {
runUndoTransparentWriteAction {
val actualSearchText = getSearchTextFromEditor(editor)
val endPos = atPos + 1 + actualSearchText.length
editor.document.deleteString(atPos, endPos)
if (shouldInsertDisplayName(lookupItem)) {
insertWithHighlight(editor, atPos, lookupItem.displayName)
}
}
}
}
private fun replaceAtSymbol(editor: Editor, lookupItem: LookupItem) {
val offset = editor.caretModel.offset
val start = findAtSymbolPosition(editor)
if (start >= 0) {
runUndoTransparentWriteAction {
val shouldInsert = shouldInsertDisplayName(lookupItem)
if (shouldInsert) {
editor.document.deleteString(start, offset)
insertWithHighlight(editor, start, lookupItem.displayName)
} else {
editor.document.deleteString(start + 1, offset)
}
}
}
}
private fun shouldInsertDisplayName(lookupItem: LookupItem): Boolean {
return lookupItem is FileActionItem
|| lookupItem is FolderActionItem
|| lookupItem is GitCommitActionItem
}
private fun insertWithHighlight(editor: Editor, position: Int, text: String) {
editor.document.insertString(position, text)
editor.caretModel.moveToOffset(position + text.length)
editor.markupModel.addRangeHighlighter(
position,
position + text.length,
HighlighterLayer.SELECTION,
TextAttributes().apply {
foregroundColor = JBColor(
PromptTextFieldConstants.LIGHT_THEME_COLOR,
PromptTextFieldConstants.DARK_THEME_COLOR
)
},
HighlighterTargetArea.EXACT_RANGE
)
}
private fun findAtSymbolPosition(editor: Editor): Int {
val atPos = editor.document.text.lastIndexOf(PromptTextFieldConstants.AT_SYMBOL)
return if (atPos >= 0) atPos else -1
}
}

View file

@ -0,0 +1,110 @@
package ee.carlrobert.codegpt.ui.textarea
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.MinusculeMatcher
import com.intellij.psi.codeStyle.NameUtil
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.group.*
import kotlinx.coroutines.CancellationException
data class SearchState(
val isInSearchContext: Boolean = false,
val isInGroupLookupContext: Boolean = false,
val lastSearchText: String? = null
)
class SearchManager(
private val project: Project,
private val tagManager: TagManager
) {
companion object {
private val logger = thisLogger()
}
fun getDefaultGroups() = listOf(
FilesGroupItem(project, tagManager),
FoldersGroupItem(project, tagManager),
GitGroupItem(project),
PersonasGroupItem(tagManager),
DocsGroupItem(tagManager),
MCPGroupItem(),
WebActionItem(tagManager)
).filter { it.enabled }
suspend fun performGlobalSearch(searchText: String): List<LookupActionItem> {
val allGroups = getDefaultGroups().filterNot { it is WebActionItem }
val allResults = mutableListOf<LookupActionItem>()
allGroups.forEach { group ->
try {
if (group is LookupGroupItem) {
val lookupActionItems =
group.getLookupItems("").filterIsInstance<LookupActionItem>()
allResults.addAll(lookupActionItems)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logger.error("Error getting results from ${group::class.simpleName}", e)
}
}
val webAction = WebActionItem(tagManager)
if (webAction.enabled()) {
allResults.add(webAction)
}
return filterAndSortResults(allResults, searchText)
}
private fun filterAndSortResults(
results: List<LookupActionItem>,
searchText: String
): List<LookupActionItem> {
val matcher: MinusculeMatcher = NameUtil.buildMatcher("*$searchText").build()
return results.mapNotNull { result ->
when (result) {
is WebActionItem -> {
if (searchText.contains("web", ignoreCase = true)) {
result to 100
} else null
}
else -> {
val matchingDegree = matcher.matchingDegree(result.displayName)
if (matchingDegree != Int.MIN_VALUE) {
result to matchingDegree
} else null
}
}
}
.sortedByDescending { it.second }
.map { it.first }
.take(PromptTextFieldConstants.MAX_SEARCH_RESULTS)
}
fun getSearchTextAfterAt(text: String, caretOffset: Int): String? {
val atPos = text.lastIndexOf(PromptTextFieldConstants.AT_SYMBOL)
if (atPos == -1 || atPos >= caretOffset) return null
val searchText = text.substring(atPos + 1, caretOffset)
return if (searchText.contains(PromptTextFieldConstants.SPACE) ||
searchText.contains(PromptTextFieldConstants.NEWLINE)
) {
null
} else {
searchText
}
}
fun matchesAnyDefaultGroup(searchText: String): Boolean {
return PromptTextFieldConstants.DEFAULT_GROUP_NAMES.any { groupName ->
groupName.startsWith(searchText, ignoreCase = true)
}
}
}

View file

@ -9,6 +9,7 @@ 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.psi.codeStyle.NameUtil
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
@ -46,9 +47,27 @@ class FilesGroupItem(
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
return readAction {
val projectFileIndex = project.service<ProjectFileIndex>()
project.service<FileEditorManager>().openFiles
.filter { projectFileIndex.isInContent(it) && !containsTag(it) }
.toFileSuggestions()
val matcher = NameUtil.buildMatcher("*$searchText").build()
val matchingFiles = mutableListOf<VirtualFile>()
projectFileIndex.iterateContent { file ->
if (!file.isDirectory &&
!containsTag(file) &&
(searchText.isEmpty() || matcher.matchingDegree(file.name) != Int.MIN_VALUE)
) {
matchingFiles.add(file)
}
true
}
val openFiles = project.service<FileEditorManager>().openFiles
.filter {
projectFileIndex.isInContent(it) &&
!containsTag(it) &&
(searchText.isEmpty() || matcher.matchingDegree(it.name) != Int.MIN_VALUE)
}
(matchingFiles + openFiles).distinctBy { it.path }.toFileSuggestions()
}
}
@ -59,7 +78,6 @@ class FilesGroupItem(
private fun Iterable<VirtualFile>.toFileSuggestions(): List<LookupActionItem> {
val selectedFileTags = TagUtil.getExistingTags(project, FileTagDetails::class.java)
return filter { file -> selectedFileTags.none { it.virtualFile == file } }
.take(10)
.map { FileActionItem(project, it) } + listOf(IncludeOpenFilesActionItem())
}
}