mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-16 19:44:36 +00:00
feat: improve @ lookup file and folder suggestions
This commit is contained in:
parent
13c337a6dc
commit
2d84f689ee
32 changed files with 1703 additions and 724 deletions
|
|
@ -0,0 +1,43 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
|
||||
data class AtLookupToken(
|
||||
val startOffset: Int,
|
||||
val endOffset: Int,
|
||||
val searchText: String
|
||||
) {
|
||||
val prefix: String
|
||||
get() = PromptTextFieldConstants.AT_SYMBOL + searchText
|
||||
|
||||
companion object {
|
||||
fun from(editor: Editor): AtLookupToken? {
|
||||
val text = editor.document.text
|
||||
return from(text, editor.caretModel.offset)
|
||||
}
|
||||
|
||||
fun from(text: String, caretOffset: Int): AtLookupToken? {
|
||||
val boundedCaretOffset = caretOffset.coerceIn(0, text.length)
|
||||
if (boundedCaretOffset == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val startOffset = text.lastIndexOf(
|
||||
PromptTextFieldConstants.AT_SYMBOL,
|
||||
boundedCaretOffset - 1
|
||||
)
|
||||
if (startOffset == -1 || startOffset >= boundedCaretOffset) {
|
||||
return null
|
||||
}
|
||||
|
||||
val searchText = text.substring(startOffset + 1, boundedCaretOffset)
|
||||
return if (searchText.contains(PromptTextFieldConstants.SPACE) ||
|
||||
searchText.contains(PromptTextFieldConstants.NEWLINE)
|
||||
) {
|
||||
null
|
||||
} else {
|
||||
AtLookupToken(startOffset, boundedCaretOffset, searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.ide.actions.searcheverywhere.FoundItemDescriptor
|
||||
import com.intellij.ide.util.gotoByName.ChooseByNameInScopeItemProvider
|
||||
import com.intellij.ide.util.gotoByName.ChooseByNamePopup
|
||||
import com.intellij.ide.util.gotoByName.ChooseByNameViewModel
|
||||
import com.intellij.ide.util.gotoByName.ChooseByNameWeightedItemProvider
|
||||
import com.intellij.ide.util.gotoByName.GotoFileModel
|
||||
import com.intellij.ide.util.gotoByName.*
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VFileProperty
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
|
|
@ -29,6 +25,17 @@ data class FileSearchCandidate(
|
|||
val source: FileSearchSource
|
||||
)
|
||||
|
||||
internal fun VirtualFile.isHiddenFileOrInHiddenDirectory(): Boolean {
|
||||
var current: VirtualFile? = this
|
||||
while (current != null) {
|
||||
if (current.name.startsWith(".") || current.`is`(VFileProperty.HIDDEN)) {
|
||||
return true
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
interface FileSearchProvider {
|
||||
suspend fun search(searchText: String, limit: Int): List<FileSearchCandidate>
|
||||
}
|
||||
|
|
@ -65,7 +72,7 @@ class NativeFileSearchProvider(private val project: Project) : FileSearchProvide
|
|||
val matches = mutableListOf<FileSearchCandidate>()
|
||||
|
||||
fun appendCandidate(file: VirtualFile): Boolean {
|
||||
if (file.isDirectory || !seenPaths.add(file.path)) {
|
||||
if (!seenPaths.add(file.path)) {
|
||||
return true
|
||||
}
|
||||
matches.add(
|
||||
|
|
@ -83,7 +90,7 @@ class NativeFileSearchProvider(private val project: Project) : FileSearchProvide
|
|||
viewModel,
|
||||
parameters,
|
||||
progressIndicator,
|
||||
Processor<FoundItemDescriptor<*>> { descriptor ->
|
||||
Processor { descriptor ->
|
||||
ProgressManager.checkCanceled()
|
||||
val file = descriptor.item.toVirtualFile() ?: return@Processor true
|
||||
appendCandidate(file)
|
||||
|
|
@ -97,7 +104,7 @@ class NativeFileSearchProvider(private val project: Project) : FileSearchProvide
|
|||
normalizedSearchText,
|
||||
false,
|
||||
progressIndicator,
|
||||
Processor<FoundItemDescriptor<*>> { descriptor ->
|
||||
Processor { descriptor ->
|
||||
ProgressManager.checkCanceled()
|
||||
val file = descriptor.item.toVirtualFile() ?: return@Processor true
|
||||
appendCandidate(file)
|
||||
|
|
@ -111,7 +118,7 @@ class NativeFileSearchProvider(private val project: Project) : FileSearchProvide
|
|||
normalizedSearchText,
|
||||
false,
|
||||
progressIndicator,
|
||||
Processor<Any> { item ->
|
||||
Processor { item ->
|
||||
ProgressManager.checkCanceled()
|
||||
val file = item.toVirtualFile() ?: return@Processor true
|
||||
appendCandidate(file)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,8 @@ package ee.carlrobert.codegpt.ui.textarea
|
|||
|
||||
object PromptTextFieldConstants {
|
||||
const val SEARCH_DELAY_MS = 200L
|
||||
const val MIN_DYNAMIC_SEARCH_LENGTH = 2
|
||||
const val LOOKUP_LOADING_REVEAL_MS = 120L
|
||||
const val LOOKUP_LOADING_MIN_VISIBLE_MS = 180L
|
||||
const val MAX_SEARCH_RESULTS = 100
|
||||
const val MIN_VISIBLE_LINES = 2
|
||||
const val DEFAULT_TOOL_WINDOW_HEIGHT = 400
|
||||
|
|
@ -13,7 +14,6 @@ object PromptTextFieldConstants {
|
|||
|
||||
val DEFAULT_GROUP_NAMES = listOf(
|
||||
"files", "file", "f",
|
||||
"folders", "folder", "fold",
|
||||
"git", "g",
|
||||
"conversations", "conversation", "conv", "c",
|
||||
"history", "hist", "h",
|
||||
|
|
@ -28,7 +28,4 @@ object PromptTextFieldConstants {
|
|||
const val AT_SYMBOL = "@"
|
||||
const val SPACE = " "
|
||||
const val NEWLINE = "\n"
|
||||
|
||||
const val LIGHT_THEME_COLOR = 0x00627A
|
||||
const val DARK_THEME_COLOR = 0xCC7832
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@ package ee.carlrobert.codegpt.ui.textarea
|
|||
|
||||
import com.intellij.ide.IdeEventQueue
|
||||
import com.intellij.openapi.application.runUndoTransparentWriteAction
|
||||
import com.intellij.openapi.ide.CopyPasteManager
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.ui.ComponentUtil.findParentByCondition
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import java.awt.AWTEvent
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import com.intellij.openapi.ide.CopyPasteManager
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
|
||||
class PromptTextFieldEventDispatcher(
|
||||
private val dispatcherId: UUID,
|
||||
private val onBackSpace: () -> Unit,
|
||||
private val onSubmit: (KeyEvent) -> Unit
|
||||
) : IdeEventQueue.EventDispatcher {
|
||||
) : IdeEventQueue.NonLockedEventDispatcher {
|
||||
|
||||
override fun dispatch(e: AWTEvent): Boolean {
|
||||
if ((e is KeyEvent || e is MouseEvent) && findParent() is PromptTextField) {
|
||||
|
|
@ -41,6 +41,7 @@ class PromptTextFieldEventDispatcher(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.VK_ENTER -> {
|
||||
val settings =
|
||||
ConfigurationSettings.getState().chatCompletionSettings
|
||||
|
|
@ -49,11 +50,15 @@ class PromptTextFieldEventDispatcher(
|
|||
|
||||
if (anyModifierConfigured) {
|
||||
var expectedModifiers = 0
|
||||
if (settings.sendWithCtrlEnter) expectedModifiers = expectedModifiers or InputEvent.CTRL_DOWN_MASK
|
||||
if (settings.sendWithAltEnter) expectedModifiers = expectedModifiers or InputEvent.ALT_DOWN_MASK
|
||||
if (settings.sendWithShiftEnter) expectedModifiers = expectedModifiers or InputEvent.SHIFT_DOWN_MASK
|
||||
if (settings.sendWithCtrlEnter) expectedModifiers =
|
||||
expectedModifiers or InputEvent.CTRL_DOWN_MASK
|
||||
if (settings.sendWithAltEnter) expectedModifiers =
|
||||
expectedModifiers or InputEvent.ALT_DOWN_MASK
|
||||
if (settings.sendWithShiftEnter) expectedModifiers =
|
||||
expectedModifiers or InputEvent.SHIFT_DOWN_MASK
|
||||
|
||||
val eventModifiers = e.modifiersEx and (InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK)
|
||||
val eventModifiers =
|
||||
e.modifiersEx and (InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK or InputEvent.SHIFT_DOWN_MASK)
|
||||
|
||||
if (eventModifiers == expectedModifiers) {
|
||||
onSubmit(e)
|
||||
|
|
@ -151,7 +156,11 @@ class PromptTextFieldEventDispatcher(
|
|||
private fun handlePaste(e: KeyEvent): Boolean {
|
||||
val parent = findParent()
|
||||
if (parent is PromptTextField) {
|
||||
val clipText: String? = try { CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor) as? String } catch (_: Exception) { null }
|
||||
val clipText: String? = try {
|
||||
CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor) as? String
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (clipText.isNullOrEmpty()) return false
|
||||
parent.insertPlaceholderFor(clipText)
|
||||
e.consume()
|
||||
|
|
|
|||
|
|
@ -1,54 +1,40 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
||||
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.ApplicationManager
|
||||
import com.intellij.openapi.application.runReadActionBlocking
|
||||
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.*
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupUtil
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.CodeAnalyzeActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.InsertsDisplayNameLookupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
|
||||
|
||||
class PromptTextFieldLookupManager(
|
||||
private val project: Project,
|
||||
private val onLookupAdded: (LookupActionItem) -> Unit
|
||||
private val onLookupAdded: (LookupActionItem) -> Unit,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val EMPTY_RESULTS_TEXT = "No results"
|
||||
}
|
||||
|
||||
fun createLookup(
|
||||
editor: Editor,
|
||||
lookupElements: Array<LookupElement>,
|
||||
searchText: String
|
||||
): LookupImpl = runReadAction {
|
||||
val lookup = LookupManager.getInstance(project).createLookup(
|
||||
searchText: String,
|
||||
): LookupImpl = runReadActionBlocking {
|
||||
LookupManager.getInstance(project).createLookup(
|
||||
editor,
|
||||
lookupElements,
|
||||
searchText,
|
||||
LookupArranger.DefaultArranger()
|
||||
) as LookupImpl
|
||||
|
||||
lookup.addLookupListener(object : LookupListener {
|
||||
override fun itemSelected(event: LookupEvent) {
|
||||
val suggestion =
|
||||
event.item?.getUserData(LookupItem.KEY) as? LookupActionItem ?: return
|
||||
|
||||
replaceAtSymbolWithSearch(editor, suggestion)
|
||||
onLookupAdded(suggestion)
|
||||
}
|
||||
})
|
||||
|
||||
lookup
|
||||
}
|
||||
|
||||
fun showGroupLookup(
|
||||
|
|
@ -59,22 +45,45 @@ class PromptTextFieldLookupManager(
|
|||
onCodeAnalyzeSelected: (CodeAnalyzeActionItem) -> Unit,
|
||||
): LookupImpl {
|
||||
val lookup = createLookup(editor, lookupElements, "")
|
||||
|
||||
lookup.addLookupListener(object : LookupListener {
|
||||
private var pendingCleanup: LookupSelectionCleanup? = null
|
||||
|
||||
override fun beforeItemSelected(event: LookupEvent): Boolean {
|
||||
pendingCleanup = createLookupSelectionCleanup(editor, event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun itemSelected(event: LookupEvent) {
|
||||
val suggestion = event.item?.getUserData(LookupItem.KEY) ?: return
|
||||
|
||||
replaceAtSymbol(editor, suggestion)
|
||||
val cleanup = pendingCleanup ?: createLookupSelectionCleanup(editor, event)
|
||||
|
||||
when (suggestion) {
|
||||
is WebActionItem -> onWebActionSelected(suggestion)
|
||||
is CodeAnalyzeActionItem -> onCodeAnalyzeSelected(suggestion)
|
||||
is LookupGroupItem -> onGroupSelected(suggestion)
|
||||
is LookupActionItem -> onLookupAdded(suggestion)
|
||||
is WebActionItem -> {
|
||||
removeLookupText(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
onWebActionSelected(suggestion)
|
||||
removeLookupTextLater(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
}
|
||||
|
||||
is CodeAnalyzeActionItem -> {
|
||||
removeLookupText(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
onCodeAnalyzeSelected(suggestion)
|
||||
removeLookupTextLater(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
}
|
||||
|
||||
is LookupGroupItem -> {
|
||||
removeLookupText(editor, cleanup, LookupCleanupMode.KEEP_AT_SYMBOL)
|
||||
onGroupSelected(suggestion) // suppress stays active until suggestion lookup opens
|
||||
removeLookupTextLater(editor, cleanup, LookupCleanupMode.KEEP_AT_SYMBOL)
|
||||
}
|
||||
|
||||
is LookupActionItem -> {
|
||||
removeLookupText(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
onLookupAdded(suggestion)
|
||||
removeLookupTextLater(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
lookup.refreshUi(false, true)
|
||||
lookup.showLookup()
|
||||
return lookup
|
||||
|
|
@ -82,163 +91,271 @@ class PromptTextFieldLookupManager(
|
|||
|
||||
fun showSearchResultsLookup(
|
||||
editor: Editor,
|
||||
results: List<LookupActionItem>,
|
||||
searchText: String
|
||||
results: List<LookupItem>,
|
||||
searchText: String,
|
||||
isCalculating: Boolean = false,
|
||||
searchTextProvider: (() -> String)? = null,
|
||||
lookupPrefix: String = searchText
|
||||
): LookupImpl {
|
||||
val lookupElements = results.toPrioritizedLookupElements(searchText)
|
||||
val lookup = createLookup(editor, lookupElements, "")
|
||||
lookup.refreshUi(false, true)
|
||||
val lookup = createLookup(editor, emptyArray(), lookupPrefix)
|
||||
updateSearchResultsLookup(
|
||||
lookup,
|
||||
results,
|
||||
searchText,
|
||||
isCalculating,
|
||||
searchTextProvider,
|
||||
matcherPrefix = lookupPrefix
|
||||
)
|
||||
addFinalActionSelectionListener(lookup, editor)
|
||||
lookup.showLookup()
|
||||
return lookup
|
||||
}
|
||||
|
||||
fun updateSearchResultsLookup(
|
||||
lookup: LookupImpl,
|
||||
results: List<LookupActionItem>
|
||||
results: List<LookupItem>,
|
||||
searchText: String,
|
||||
isCalculating: Boolean = false,
|
||||
searchTextProvider: (() -> String)? = null,
|
||||
matcherPrefix: String = searchText
|
||||
): Int {
|
||||
val existingKeys = lookup.items.mapNotNull { element ->
|
||||
(element.getUserData(LookupItem.KEY) as? LookupActionItem)?.let(::resultKey)
|
||||
}.toMutableSet()
|
||||
val newResults = results.filter { result -> existingKeys.add(resultKey(result)) }
|
||||
if (newResults.isEmpty()) {
|
||||
lookup.refreshUi(false, true)
|
||||
return 0
|
||||
return replaceLookupItems(
|
||||
lookup,
|
||||
results,
|
||||
searchText,
|
||||
isCalculating,
|
||||
searchTextProvider,
|
||||
matcherPrefix
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSuggestionLookup(
|
||||
lookup: LookupImpl,
|
||||
results: List<LookupItem>,
|
||||
searchText: String,
|
||||
isCalculating: Boolean = false,
|
||||
searchTextProvider: (() -> String)? = null,
|
||||
matcherPrefix: String = searchText
|
||||
): Int {
|
||||
return replaceLookupItems(
|
||||
lookup,
|
||||
results,
|
||||
searchText,
|
||||
isCalculating,
|
||||
searchTextProvider,
|
||||
matcherPrefix
|
||||
)
|
||||
}
|
||||
|
||||
fun getSelectedLookupItemKey(lookup: LookupImpl): String? {
|
||||
val currentItem = lookup.currentItem ?: return null
|
||||
val lookupItem = currentItem.getUserData(LookupItem.KEY) ?: return null
|
||||
return resultKey(lookupItem)
|
||||
}
|
||||
|
||||
fun restoreSelectedLookupItem(
|
||||
lookup: LookupImpl,
|
||||
selectedKey: String?
|
||||
) {
|
||||
if (selectedKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
LookupUtil.addLookupItems(
|
||||
lookup,
|
||||
newResults.mapIndexed { index, result ->
|
||||
result to resultPriority(result, index, newResults.size)
|
||||
},
|
||||
getSearchTextFromLookup(lookup)
|
||||
)
|
||||
return newResults.size
|
||||
val matchingItem = lookup.items.firstOrNull { element ->
|
||||
val lookupItem = element.getUserData(LookupItem.KEY) ?: return@firstOrNull false
|
||||
resultKey(lookupItem) == selectedKey
|
||||
} ?: return
|
||||
|
||||
lookup.currentItem = matchingItem
|
||||
lookup.ensureSelectionVisible(false)
|
||||
}
|
||||
|
||||
fun showSuggestionLookup(
|
||||
editor: Editor,
|
||||
lookupElements: Array<LookupElement>,
|
||||
parentGroup: LookupGroupItem,
|
||||
onDynamicUpdate: (String) -> Unit
|
||||
lookupItems: List<LookupItem>,
|
||||
searchText: String = "",
|
||||
isCalculating: Boolean = false,
|
||||
searchTextProvider: (() -> String)? = null,
|
||||
lookupPrefix: String = searchText,
|
||||
): LookupImpl {
|
||||
val lookup = createLookup(editor, lookupElements, "")
|
||||
if (parentGroup is DynamicLookupGroupItem) {
|
||||
setupDynamicLookupListener(lookup, onDynamicUpdate)
|
||||
}
|
||||
|
||||
lookup.refreshUi(false, true)
|
||||
val lookup = createLookup(editor, emptyArray(), lookupPrefix)
|
||||
updateSuggestionLookup(
|
||||
lookup,
|
||||
lookupItems,
|
||||
searchText,
|
||||
isCalculating,
|
||||
searchTextProvider,
|
||||
matcherPrefix = lookupPrefix
|
||||
)
|
||||
addFinalActionSelectionListener(lookup, editor)
|
||||
lookup.showLookup()
|
||||
return lookup
|
||||
}
|
||||
|
||||
private fun setupDynamicLookupListener(
|
||||
private fun addFinalActionSelectionListener(
|
||||
lookup: LookupImpl,
|
||||
onDynamicUpdate: (String) -> Unit
|
||||
editor: Editor
|
||||
) {
|
||||
lookup.addPrefixChangeListener(object : PrefixChangeListener {
|
||||
override fun afterAppend(c: Char) {
|
||||
val searchText = getSearchTextFromLookup(lookup)
|
||||
if (searchText.length >= PromptTextFieldConstants.MIN_DYNAMIC_SEARCH_LENGTH) {
|
||||
onDynamicUpdate(searchText)
|
||||
}
|
||||
lookup.addLookupListener(object : LookupListener {
|
||||
private var pendingCleanup: LookupSelectionCleanup? = null
|
||||
|
||||
override fun beforeItemSelected(event: LookupEvent): Boolean {
|
||||
pendingCleanup = createLookupSelectionCleanup(editor, event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun afterTruncate() {
|
||||
val searchText = getSearchTextFromLookup(lookup)
|
||||
if (searchText.isEmpty()) {
|
||||
onDynamicUpdate("")
|
||||
}
|
||||
override fun itemSelected(event: LookupEvent) {
|
||||
val suggestion =
|
||||
event.item?.getUserData(LookupItem.KEY) as? LookupActionItem ?: return
|
||||
val cleanup = pendingCleanup ?: createLookupSelectionCleanup(editor, event)
|
||||
removeLookupText(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
onLookupAdded(suggestion)
|
||||
removeLookupTextLater(editor, cleanup, LookupCleanupMode.REMOVE_TOKEN)
|
||||
}
|
||||
}, 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 in 0..<caretOffset) {
|
||||
text.substring(atIndex + 1, caretOffset)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceAtSymbolWithSearch(
|
||||
private fun createLookupSelectionCleanup(
|
||||
editor: Editor,
|
||||
lookupItem: LookupItem
|
||||
event: LookupEvent
|
||||
): LookupSelectionCleanup {
|
||||
val token = AtLookupToken.from(editor)
|
||||
val item = event.item
|
||||
val lookupItem = item?.getUserData(LookupItem.KEY)
|
||||
val lookupStrings = buildSet {
|
||||
item?.lookupString?.let(::add)
|
||||
item?.allLookupStrings?.let(::addAll)
|
||||
lookupItem?.displayName?.let(::add)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
.sortedByDescending { it.length }
|
||||
|
||||
return LookupSelectionCleanup(token?.startOffset, lookupStrings)
|
||||
}
|
||||
|
||||
private fun removeLookupTextLater(
|
||||
editor: Editor,
|
||||
cleanup: LookupSelectionCleanup?,
|
||||
mode: LookupCleanupMode
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
removeLookupText(editor, cleanup, mode)
|
||||
}
|
||||
}
|
||||
|
||||
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 removeLookupText(
|
||||
editor: Editor,
|
||||
cleanup: LookupSelectionCleanup?,
|
||||
mode: LookupCleanupMode
|
||||
) {
|
||||
runUndoTransparentWriteAction {
|
||||
val token = AtLookupToken.from(editor)
|
||||
val anchorOffset = token?.startOffset ?: cleanup?.startOffset ?: editor.caretModel.offset
|
||||
if (token != null) {
|
||||
when (mode) {
|
||||
LookupCleanupMode.REMOVE_TOKEN -> {
|
||||
editor.document.deleteString(token.startOffset, token.endOffset)
|
||||
editor.caretModel.moveToOffset(token.startOffset)
|
||||
}
|
||||
|
||||
LookupCleanupMode.KEEP_AT_SYMBOL -> {
|
||||
editor.document.deleteString(token.startOffset + 1, token.endOffset)
|
||||
editor.caretModel.moveToOffset(token.startOffset + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val labelOffset = when (mode) {
|
||||
LookupCleanupMode.REMOVE_TOKEN -> anchorOffset
|
||||
LookupCleanupMode.KEEP_AT_SYMBOL -> (anchorOffset + 1).coerceAtMost(editor.document.textLength)
|
||||
}
|
||||
cleanup?.lookupStrings.orEmpty().firstOrNull {
|
||||
removeLookupStringAt(editor, labelOffset, it)
|
||||
} ?: removeLookupStringBeforeCaret(editor, cleanup?.lookupStrings.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldInsertDisplayName(lookupItem: LookupItem): Boolean {
|
||||
return lookupItem is FileActionItem
|
||||
|| lookupItem is FolderActionItem
|
||||
|| lookupItem is InsertsDisplayNameLookupItem
|
||||
private fun removeLookupStringAt(
|
||||
editor: Editor,
|
||||
offset: Int,
|
||||
lookupString: String
|
||||
): Boolean {
|
||||
val document = editor.document
|
||||
if (offset < 0 || offset + lookupString.length > document.textLength) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.charsSequence.substring(offset, offset + lookupString.length) != lookupString) {
|
||||
return false
|
||||
}
|
||||
|
||||
document.deleteString(offset, offset + lookupString.length)
|
||||
editor.caretModel.moveToOffset(offset)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun List<LookupActionItem>.toPrioritizedLookupElements(
|
||||
searchText: String
|
||||
): Array<LookupElement> {
|
||||
return mapIndexed { index, result ->
|
||||
PrioritizedLookupElement.withPriority(
|
||||
result.createLookupElement(searchText),
|
||||
resultPriority(result, index, size)
|
||||
)
|
||||
}.toTypedArray()
|
||||
private fun removeLookupStringBeforeCaret(
|
||||
editor: Editor,
|
||||
lookupStrings: List<String>
|
||||
) {
|
||||
val caretOffset = editor.caretModel.offset
|
||||
lookupStrings.firstOrNull { lookupString ->
|
||||
val startOffset = caretOffset - lookupString.length
|
||||
removeLookupStringAt(editor, startOffset, lookupString)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceLookupItems(
|
||||
lookup: LookupImpl,
|
||||
results: List<LookupItem>,
|
||||
searchText: String,
|
||||
isCalculating: Boolean,
|
||||
searchTextProvider: (() -> String)?,
|
||||
matcherPrefix: String
|
||||
): Int {
|
||||
if (lookup.isLookupDisposed) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val selectedKey = getSelectedLookupItemKey(lookup)
|
||||
lookup.arranger = LookupArranger.DefaultArranger()
|
||||
LookupUtil.addLookupItems(
|
||||
lookup,
|
||||
results.mapIndexed { index, result ->
|
||||
result to resultPriority(result, index, results.size)
|
||||
},
|
||||
searchText,
|
||||
searchTextProvider,
|
||||
matcherPrefix
|
||||
)
|
||||
configureLookup(lookup, isCalculating)
|
||||
restoreSelectedLookupItem(lookup, selectedKey)
|
||||
return results.size
|
||||
}
|
||||
|
||||
private fun configureLookup(
|
||||
lookup: LookupImpl,
|
||||
isCalculating: Boolean
|
||||
) {
|
||||
lookup.isStartCompletionWhenNothingMatches = true
|
||||
lookup.dummyItemCount = 0
|
||||
lookup.isCalculating = isCalculating
|
||||
lookup.refreshUi(false, true)
|
||||
}
|
||||
|
||||
private fun resultPriority(
|
||||
result: LookupActionItem,
|
||||
result: LookupItem,
|
||||
index: Int,
|
||||
total: Int
|
||||
): Double {
|
||||
val sourcePriority = when (result) {
|
||||
is FileActionItem -> when (result.source) {
|
||||
FileSearchSource.NATIVE -> 3_000.0
|
||||
FileSearchSource.OPEN -> 2_500.0
|
||||
FileSearchSource.RECENT -> 2_000.0
|
||||
}
|
||||
|
||||
else -> 1_000.0
|
||||
return if (result is StatusLookupItem || result is LoadingLookupItem) {
|
||||
0.0
|
||||
} else {
|
||||
(total - index).toDouble()
|
||||
}
|
||||
return sourcePriority - index.toDouble() / maxOf(total, 1)
|
||||
}
|
||||
|
||||
private fun resultKey(result: LookupActionItem): String {
|
||||
private fun resultKey(result: LookupItem): String {
|
||||
return when (result) {
|
||||
is FileActionItem -> "file:${result.file.path}"
|
||||
is FolderActionItem -> "folder:${result.folder.path}"
|
||||
|
|
@ -246,25 +363,13 @@ class PromptTextFieldLookupManager(
|
|||
}
|
||||
}
|
||||
|
||||
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 data class LookupSelectionCleanup(
|
||||
val startOffset: Int?,
|
||||
val lookupStrings: List<String>
|
||||
)
|
||||
|
||||
private fun findAtSymbolPosition(editor: Editor): Int {
|
||||
val atPos = editor.document.text.lastIndexOf(PromptTextFieldConstants.AT_SYMBOL)
|
||||
return if (atPos >= 0) atPos else -1
|
||||
private enum class LookupCleanupMode {
|
||||
REMOVE_TOKEN,
|
||||
KEEP_AT_SYMBOL
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ 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.settings.service.FeatureType
|
||||
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.LookupMatchers
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.ImageActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.IncludeOpenFilesActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.IncludeCurrentChangesActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.group.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
|
@ -29,7 +30,9 @@ class SearchManager(
|
|||
private val featureType: FeatureType? = null,
|
||||
) {
|
||||
private val fileSearchProvider = NativeFileSearchProvider(project)
|
||||
private val foldersGroupItem = FoldersGroupItem(project, tagManager)
|
||||
private val filesGroupItem = FilesGroupItem(project, tagManager, fileSearchProvider)
|
||||
private var cachedSearchText: String = ""
|
||||
private var cachedSearchResults: List<LookupActionItem> = emptyList()
|
||||
|
||||
companion object {
|
||||
private val logger = thisLogger()
|
||||
|
|
@ -42,16 +45,14 @@ class SearchManager(
|
|||
}
|
||||
|
||||
private fun getInlineEditGroups() = listOfNotNull(
|
||||
FilesGroupItem(project, tagManager, fileSearchProvider),
|
||||
foldersGroupItem,
|
||||
filesGroupItem,
|
||||
if (GitFeatureAvailability.isAvailable) GitGroupItem(project) else null,
|
||||
HistoryGroupItem(),
|
||||
DiagnosticsGroupItem(tagManager)
|
||||
).filter { it.enabled }
|
||||
|
||||
private fun getAgentGroups() = listOfNotNull(
|
||||
FilesGroupItem(project, tagManager, fileSearchProvider),
|
||||
foldersGroupItem,
|
||||
filesGroupItem,
|
||||
if (GitFeatureAvailability.isAvailable) GitGroupItem(project) else null,
|
||||
MCPGroupItem(tagManager),
|
||||
DiagnosticsGroupItem(tagManager),
|
||||
|
|
@ -59,8 +60,7 @@ class SearchManager(
|
|||
).filter { it.enabled }
|
||||
|
||||
private fun getAllGroups() = listOfNotNull(
|
||||
FilesGroupItem(project, tagManager, fileSearchProvider),
|
||||
foldersGroupItem,
|
||||
filesGroupItem,
|
||||
if (GitFeatureAvailability.isAvailable) GitGroupItem(project) else null,
|
||||
HistoryGroupItem(),
|
||||
PersonasGroupItem(tagManager),
|
||||
|
|
@ -74,17 +74,12 @@ class SearchManager(
|
|||
val groups = getDefaultGroups()
|
||||
val results = mutableListOf<LookupActionItem>()
|
||||
|
||||
// Standalone action items that are normally buried inside heavy groups
|
||||
if (groups.any { it is FilesGroupItem }) {
|
||||
results.add(IncludeOpenFilesActionItem())
|
||||
}
|
||||
if (GitFeatureAvailability.isAvailable && groups.any { it is GitGroupItem }) {
|
||||
results.add(IncludeCurrentChangesActionItem())
|
||||
}
|
||||
|
||||
// Lightweight groups (in-memory data only)
|
||||
val lightGroups = groups
|
||||
.filterNot { it is FilesGroupItem || it is FoldersGroupItem || it is GitGroupItem }
|
||||
.filterNot { it is FilesGroupItem || it is GitGroupItem }
|
||||
.filterNot { it is WebActionItem || it is ImageActionItem }
|
||||
|
||||
lightGroups.forEach { group ->
|
||||
|
|
@ -114,17 +109,9 @@ class SearchManager(
|
|||
suspend fun performFileSearch(searchText: String): List<LookupActionItem> {
|
||||
val fileGroup = getDefaultGroups().filterIsInstance<FilesGroupItem>().firstOrNull()
|
||||
?: return emptyList()
|
||||
val matcher = createMatcher(searchText)
|
||||
|
||||
return try {
|
||||
fileGroup.getLookupItems(searchText)
|
||||
.filter { result ->
|
||||
result !is IncludeOpenFilesActionItem || matchesSearchText(
|
||||
result,
|
||||
searchText,
|
||||
matcher
|
||||
)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -136,16 +123,12 @@ class SearchManager(
|
|||
suspend fun performDeferredHeavySearch(searchText: String): List<LookupActionItem> =
|
||||
coroutineScope {
|
||||
val deferredGroups = getDefaultGroups()
|
||||
.filter { it is FoldersGroupItem || it is GitGroupItem }
|
||||
.filterIsInstance<GitGroupItem>()
|
||||
|
||||
deferredGroups.map { group ->
|
||||
async {
|
||||
try {
|
||||
if (group is LookupGroupItem) {
|
||||
group.getLookupItems(searchText).filterIsInstance<LookupActionItem>()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
group.getLookupItems(searchText)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -165,13 +148,36 @@ class SearchManager(
|
|||
searchText: String
|
||||
): List<LookupActionItem> {
|
||||
val seenKeys = mutableSetOf<String>()
|
||||
val orderedPrimary = primaryResults
|
||||
.filter { candidate -> seenKeys.add(resultKey(candidate)) }
|
||||
val orderedSecondary = filterAndSortResults(secondaryResults, searchText)
|
||||
val combined = (primaryResults + secondaryResults)
|
||||
.filter { candidate -> seenKeys.add(resultKey(candidate)) }
|
||||
|
||||
return (orderedPrimary + orderedSecondary)
|
||||
.take(PromptTextFieldConstants.MAX_SEARCH_RESULTS)
|
||||
return filterAndSortResults(combined, searchText)
|
||||
}
|
||||
|
||||
fun getOptimisticSearchResults(searchText: String): List<LookupActionItem> {
|
||||
val normalizedSearchText = searchText.trim()
|
||||
val cachedText = cachedSearchText
|
||||
if (normalizedSearchText.isEmpty() ||
|
||||
cachedText.isEmpty() ||
|
||||
!normalizedSearchText.startsWith(cachedText, ignoreCase = true)
|
||||
) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return filterAndSortResults(cachedSearchResults, normalizedSearchText)
|
||||
}
|
||||
|
||||
fun cacheSearchResults(
|
||||
searchText: String,
|
||||
results: List<LookupActionItem>
|
||||
) {
|
||||
cachedSearchText = searchText.trim()
|
||||
cachedSearchResults = results
|
||||
}
|
||||
|
||||
fun clearSearchResultsCache() {
|
||||
cachedSearchText = ""
|
||||
cachedSearchResults = emptyList()
|
||||
}
|
||||
|
||||
suspend fun performGlobalSearch(searchText: String): List<LookupActionItem> {
|
||||
|
|
@ -200,15 +206,7 @@ class SearchManager(
|
|||
}
|
||||
|
||||
private fun createMatcher(searchText: String): MinusculeMatcher {
|
||||
return NameUtil.buildMatcher("*$searchText").build()
|
||||
}
|
||||
|
||||
private fun matchesSearchText(
|
||||
result: LookupActionItem,
|
||||
searchText: String,
|
||||
matcher: MinusculeMatcher
|
||||
): Boolean {
|
||||
return getMatchingDegree(result, searchText, matcher) != Int.MIN_VALUE
|
||||
return LookupMatchers.createMatcher(searchText)
|
||||
}
|
||||
|
||||
private fun getMatchingDegree(
|
||||
|
|
@ -225,6 +223,20 @@ class SearchManager(
|
|||
}
|
||||
}
|
||||
|
||||
is FileActionItem -> {
|
||||
maxOf(
|
||||
matcher.matchingDegree(result.displayName),
|
||||
matcher.matchingDegree(result.file.path)
|
||||
)
|
||||
}
|
||||
|
||||
is FolderActionItem -> {
|
||||
maxOf(
|
||||
matcher.matchingDegree(result.displayName),
|
||||
matcher.matchingDegree(result.folder.path)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
matcher.matchingDegree(result.displayName)
|
||||
}
|
||||
|
|
@ -233,10 +245,10 @@ class SearchManager(
|
|||
|
||||
private fun resultKey(result: LookupActionItem): String {
|
||||
return when (result) {
|
||||
is ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem ->
|
||||
is FileActionItem ->
|
||||
"file:${result.file.path}"
|
||||
|
||||
is ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem ->
|
||||
is FolderActionItem ->
|
||||
"folder:${result.folder.path}"
|
||||
|
||||
else -> "${result::class.qualifiedName}:${result.displayName}"
|
||||
|
|
@ -244,17 +256,7 @@ class SearchManager(
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
return AtLookupToken.from(text, caretOffset)?.searchText
|
||||
}
|
||||
|
||||
fun matchesAnyDefaultGroup(searchText: String): Boolean {
|
||||
|
|
@ -262,5 +264,4 @@ class SearchManager(
|
|||
groupName.startsWith(searchText, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import ee.carlrobert.codegpt.conversations.Conversation
|
|||
import ee.carlrobert.codegpt.conversations.ConversationsState
|
||||
import ee.carlrobert.codegpt.conversations.message.Message
|
||||
import ee.carlrobert.codegpt.diagnostics.ProjectDiagnosticsService
|
||||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatContextSupport
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.HistoryActionItem
|
||||
import ee.carlrobert.codegpt.util.EditorUtil
|
||||
|
|
@ -39,39 +39,39 @@ object TagProcessorFactory {
|
|||
is DiagnosticsTagDetails -> DiagnosticsTagProcessor(project, tagDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileTagProcessor(
|
||||
project: Project,
|
||||
private val tagDetails: FileTagDetails,
|
||||
) : TagProcessor {
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
|
||||
override fun process(message: Message, promptBuilder: StringBuilder) {
|
||||
if (!settingsService.isVirtualFileVisible(tagDetails.virtualFile)) {
|
||||
internal fun appendReferencedFilePaths(
|
||||
project: Project,
|
||||
message: Message,
|
||||
virtualFiles: List<VirtualFile>
|
||||
) {
|
||||
val referencedPaths = ChatContextSupport.collectVisibleFiles(project, virtualFiles)
|
||||
.map(VirtualFile::getPath)
|
||||
if (referencedPaths.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (message.referencedFilePaths == null) {
|
||||
message.referencedFilePaths = mutableListOf()
|
||||
}
|
||||
message.referencedFilePaths?.add(tagDetails.virtualFile.path)
|
||||
message.referencedFilePaths?.addAll(referencedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
class FileTagProcessor(
|
||||
private val project: Project,
|
||||
private val tagDetails: FileTagDetails,
|
||||
) : TagProcessor {
|
||||
override fun process(message: Message, promptBuilder: StringBuilder) {
|
||||
TagProcessorFactory.appendReferencedFilePaths(project, message, listOf(tagDetails.virtualFile))
|
||||
}
|
||||
}
|
||||
|
||||
class EditorTagProcessor(
|
||||
project: Project,
|
||||
private val project: Project,
|
||||
private val tagDetails: EditorTagDetails,
|
||||
) : TagProcessor {
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
|
||||
override fun process(message: Message, promptBuilder: StringBuilder) {
|
||||
if (!settingsService.isVirtualFileVisible(tagDetails.virtualFile)) {
|
||||
return
|
||||
}
|
||||
if (message.referencedFilePaths == null) {
|
||||
message.referencedFilePaths = mutableListOf()
|
||||
}
|
||||
message.referencedFilePaths?.add(tagDetails.virtualFile.path)
|
||||
TagProcessorFactory.appendReferencedFilePaths(project, message, listOf(tagDetails.virtualFile))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,32 +117,14 @@ class PersonaTagProcessor(
|
|||
}
|
||||
|
||||
class FolderTagProcessor(
|
||||
project: Project,
|
||||
private val project: Project,
|
||||
private val tagDetails: FolderTagDetails,
|
||||
) : TagProcessor {
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
|
||||
override fun process(
|
||||
message: Message,
|
||||
promptBuilder: StringBuilder
|
||||
) {
|
||||
if (message.referencedFilePaths == null) {
|
||||
message.referencedFilePaths = mutableListOf()
|
||||
}
|
||||
|
||||
processFolder(tagDetails.folder, message.referencedFilePaths ?: mutableListOf())
|
||||
}
|
||||
|
||||
private fun processFolder(folder: VirtualFile, referencedFilePaths: MutableList<String>) {
|
||||
folder.children.forEach { child ->
|
||||
if (!settingsService.isVirtualFileVisible(child)) {
|
||||
return@forEach
|
||||
}
|
||||
when {
|
||||
child.isDirectory -> processFolder(child, referencedFilePaths)
|
||||
else -> referencedFilePaths.add(child.path)
|
||||
}
|
||||
}
|
||||
TagProcessorFactory.appendReferencedFilePaths(project, message, listOf(tagDetails.folder))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import java.awt.geom.Rectangle2D
|
|||
import java.awt.geom.RoundRectangle2D
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
|
||||
class UserInputPanel @JvmOverloads constructor(
|
||||
|
|
@ -104,6 +105,7 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
onTextChanged = ::updateUserTokens,
|
||||
onBackSpace = ::handleBackSpace,
|
||||
onLookupAdded = ::handleLookupAdded,
|
||||
onLookupSearchLoadingChanged = ::setLookupSearchVisible,
|
||||
onSubmit = ::handleSubmit,
|
||||
onFilesDropped = { files ->
|
||||
includeFiles(files.toMutableList())
|
||||
|
|
@ -140,8 +142,23 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
InlineEditChips.rejectAll { onRejectAll?.invoke() }.apply { isVisible = false }
|
||||
private var inlineEditControls: List<JComponent> = listOf(acceptChip, rejectChip)
|
||||
|
||||
private val lookupSearchIcon = AsyncProcessIcon("lookup-search").apply {
|
||||
isVisible = false
|
||||
toolTipText = "Searching..."
|
||||
}
|
||||
private val lookupSearchPanel =
|
||||
JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
|
||||
isOpaque = false
|
||||
add(lookupSearchIcon)
|
||||
val iconSize = lookupSearchIcon.preferredSize
|
||||
minimumSize = iconSize
|
||||
preferredSize = iconSize
|
||||
maximumSize = iconSize
|
||||
toolTipText = "Searching..."
|
||||
isVisible = true
|
||||
}
|
||||
private val thinkingIcon = AsyncProcessIcon("inline-edit-thinking").apply { isVisible = false }
|
||||
private val thinkingLabel = javax.swing.JLabel(CodeGPTBundle.get("shared.thinking")).apply {
|
||||
private val thinkingLabel = JLabel(CodeGPTBundle.get("shared.thinking")).apply {
|
||||
foreground = service<EditorColorsManager>().globalScheme.defaultForeground
|
||||
isVisible = false
|
||||
}
|
||||
|
|
@ -635,6 +652,7 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
if (agentTokenCounterPanel != null) {
|
||||
cell(agentTokenCounterPanel).gap(RightGap.SMALL)
|
||||
}
|
||||
cell(lookupSearchPanel).gap(RightGap.SMALL)
|
||||
cell(thinkingPanel).gap(RightGap.SMALL)
|
||||
cell(acceptChip).gap(RightGap.SMALL)
|
||||
cell(rejectChip).gap(RightGap.SMALL)
|
||||
|
|
@ -668,6 +686,12 @@ class UserInputPanel @JvmOverloads constructor(
|
|||
return pnl
|
||||
}
|
||||
|
||||
private fun setLookupSearchVisible(visible: Boolean) {
|
||||
lookupSearchIcon.isVisible = visible
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun setInlineEditControlsVisible(visible: Boolean) {
|
||||
inlineEditControls.forEach { it.isVisible = visible }
|
||||
revalidate()
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import com.intellij.util.IconUtil
|
|||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.EditorNotifier
|
||||
import ee.carlrobert.codegpt.EncodingManager
|
||||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatContextSupport
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import ee.carlrobert.codegpt.ui.WrapLayout
|
||||
|
|
@ -35,7 +35,6 @@ import ee.carlrobert.codegpt.ui.textarea.TagDetailsComparator
|
|||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
import ee.carlrobert.codegpt.util.EditorUtil
|
||||
import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor
|
||||
import ee.carlrobert.codegpt.util.file.FileUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -178,35 +177,21 @@ class UserInputHeaderPanel(
|
|||
|
||||
override fun onTagAdded(tag: TagDetails) {
|
||||
onTagsChanged()
|
||||
when (tag) {
|
||||
is FileTagDetails -> if (tag.selected) adjustReferencedTotal(
|
||||
tag.virtualFile,
|
||||
add = true
|
||||
)
|
||||
|
||||
is EditorTagDetails -> if (tag.selected) adjustReferencedTotal(
|
||||
tag.virtualFile,
|
||||
add = true
|
||||
)
|
||||
|
||||
else -> Unit
|
||||
if (affectsReferencedFiles(tag)) {
|
||||
refreshReferencedFilesTotal()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTagRemoved(tag: TagDetails) {
|
||||
onTagsChanged()
|
||||
when (tag) {
|
||||
is FileTagDetails -> if (tag.selected) adjustReferencedTotal(
|
||||
tag.virtualFile,
|
||||
add = false
|
||||
)
|
||||
if (affectsReferencedFiles(tag)) {
|
||||
refreshReferencedFilesTotal()
|
||||
}
|
||||
}
|
||||
|
||||
is EditorTagDetails -> if (tag.selected) adjustReferencedTotal(
|
||||
tag.virtualFile,
|
||||
add = false
|
||||
)
|
||||
|
||||
else -> Unit
|
||||
override fun onTagUpdated(tag: TagDetails) {
|
||||
if (affectsReferencedFiles(tag)) {
|
||||
refreshReferencedFilesTotal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,19 +254,6 @@ class UserInputHeaderPanel(
|
|||
override fun onSelect(tagDetails: TagDetails) {
|
||||
SwingUtilities.invokeLater {
|
||||
onTagsChanged()
|
||||
when (tagDetails) {
|
||||
is FileTagDetails -> adjustReferencedTotal(
|
||||
tagDetails.virtualFile,
|
||||
add = isSelected
|
||||
)
|
||||
|
||||
is EditorTagDetails -> adjustReferencedTotal(
|
||||
tagDetails.virtualFile,
|
||||
add = isSelected
|
||||
)
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
tagManager.notifyTagUpdated(tagDetails)
|
||||
}
|
||||
}
|
||||
|
|
@ -304,20 +276,6 @@ class UserInputHeaderPanel(
|
|||
addInitialTags()
|
||||
}
|
||||
|
||||
fun setApplyVisible(visible: Boolean) {
|
||||
applyChip?.isVisible = visible
|
||||
revalidate()
|
||||
repaint()
|
||||
onLayoutChanged()
|
||||
}
|
||||
|
||||
fun setApplyEnabled(enabled: Boolean) {
|
||||
applyChip?.isEnabled = enabled
|
||||
revalidate()
|
||||
repaint()
|
||||
onLayoutChanged()
|
||||
}
|
||||
|
||||
private fun addInitialTags() {
|
||||
if (featureType == FeatureType.AGENT) {
|
||||
return
|
||||
|
|
@ -345,19 +303,22 @@ class UserInputHeaderPanel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun adjustReferencedTotal(virtualFile: VirtualFile, add: Boolean) {
|
||||
private fun refreshReferencedFilesTotal() {
|
||||
backgroundScope.launch {
|
||||
val encodingManager = EncodingManager.getInstance()
|
||||
val content = FileUtil.readContent(virtualFile)
|
||||
val tokens = encodingManager.countTokens(content)
|
||||
val selectedTags = getSelectedTags()
|
||||
val referencedFileContents =
|
||||
ChatContextSupport.getReferencedFiles(project, selectedTags)
|
||||
.map { it.fileContent }
|
||||
runInEdt {
|
||||
val current = totalTokensPanel.getTokenDetails().referencedFilesTokens
|
||||
val next = if (add) current + tokens else (current - tokens).coerceAtLeast(0)
|
||||
totalTokensPanel.updateReferencedFilesTokens(next)
|
||||
totalTokensPanel.updateReferencedFilesTokens(referencedFileContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun affectsReferencedFiles(tag: TagDetails): Boolean {
|
||||
return tag is FileTagDetails || tag is EditorTagDetails || tag is FolderTagDetails
|
||||
}
|
||||
|
||||
private fun initializeEventListeners() {
|
||||
project.messageBus.connect().apply {
|
||||
subscribe(EditorNotifier.SelectionChange.TOPIC, EditorSelectionChangeListener())
|
||||
|
|
|
|||
|
|
@ -26,18 +26,14 @@ class TagManager {
|
|||
.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 containsTag(file: VirtualFile): Boolean = tags.any { tag ->
|
||||
when (tag) {
|
||||
is SelectionTagDetails -> tag.virtualFile == file
|
||||
is FileTagDetails -> tag.virtualFile == file
|
||||
is EditorSelectionTagDetails -> tag.virtualFile == file
|
||||
is EditorTagDetails -> tag.virtualFile == file
|
||||
is FolderTagDetails -> tag.folder == file
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,40 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup
|
||||
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
||||
import com.intellij.codeInsight.completion.InsertionContext
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder
|
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation
|
||||
import com.intellij.psi.codeStyle.NameUtil
|
||||
import com.intellij.codeInsight.lookup.LookupElementRenderer
|
||||
import com.intellij.openapi.util.TextRange
|
||||
|
||||
abstract class AbstractLookupItem : LookupItem {
|
||||
override fun createLookupElement(searchText: String): LookupElement {
|
||||
val lookupElement = LookupElementBuilder.create(getLookupString())
|
||||
override fun createLookupElement(
|
||||
searchText: String,
|
||||
searchTextProvider: (() -> String)?
|
||||
): LookupElement {
|
||||
var builder = LookupElementBuilder.create(getLookupString())
|
||||
.withPresentableText(displayName)
|
||||
.withIcon(icon)
|
||||
.withInsertHandler { context, _ -> removeInsertedLookupText(context) }
|
||||
.withRenderer(object : LookupElementRenderer<LookupElement>() {
|
||||
override fun renderElement(
|
||||
element: LookupElement,
|
||||
presentation: LookupElementPresentation
|
||||
) {
|
||||
setPresentation(element, presentation)
|
||||
emphasizeMatch(presentation, searchText)
|
||||
emphasizeMatch(
|
||||
presentation,
|
||||
searchTextProvider?.invoke() ?: searchText
|
||||
)
|
||||
}
|
||||
})
|
||||
.apply {
|
||||
putUserData(LookupItem.KEY, this@AbstractLookupItem)
|
||||
}
|
||||
getAdditionalLookupStrings().forEach { lookupString ->
|
||||
builder = builder.withLookupString(lookupString)
|
||||
}
|
||||
val lookupElement = builder.apply {
|
||||
putUserData(LookupItem.KEY, this@AbstractLookupItem)
|
||||
}
|
||||
return PrioritizedLookupElement.withPriority(lookupElement, 1.0)
|
||||
}
|
||||
|
||||
|
|
@ -36,11 +47,28 @@ abstract class AbstractLookupItem : LookupItem {
|
|||
return
|
||||
}
|
||||
|
||||
val matcher = NameUtil.buildMatcher("*$normalizedSearchText").build()
|
||||
if (matcher.matchingFragments(displayName) != null) {
|
||||
presentation.isItemTextBold = true
|
||||
val matcher = LookupMatchers.createMatcher(normalizedSearchText)
|
||||
matcher.match(displayName)?.forEach { fragment ->
|
||||
presentation.decorateItemTextRange(
|
||||
TextRange(fragment.startOffset, fragment.endOffset),
|
||||
LookupElementPresentation.LookupItemDecoration.HIGHLIGHT_MATCHED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLookupString(): String
|
||||
|
||||
protected open fun getAdditionalLookupStrings(): Collection<String> = emptyList()
|
||||
|
||||
private fun removeInsertedLookupText(context: InsertionContext) {
|
||||
val startOffset = context.startOffset
|
||||
val tailOffset = context.tailOffset
|
||||
val document = context.document
|
||||
if (startOffset in 0..tailOffset && tailOffset <= document.textLength) {
|
||||
document.deleteString(startOffset, tailOffset)
|
||||
context.editor.caretModel.moveToOffset(startOffset)
|
||||
context.tailOffset = startOffset
|
||||
}
|
||||
context.setAddCompletionChar(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup
|
||||
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
|
||||
class LoadingLookupItem(
|
||||
private val lookupString: String
|
||||
) : AbstractLookupItem() {
|
||||
|
||||
override val displayName = "Searching..."
|
||||
override val icon = AnimatedIcon.Default()
|
||||
override val enabled = false
|
||||
|
||||
override fun setPresentation(
|
||||
element: LookupElement,
|
||||
presentation: LookupElementPresentation
|
||||
) {
|
||||
presentation.itemText = displayName
|
||||
presentation.icon = icon
|
||||
presentation.isItemTextBold = false
|
||||
}
|
||||
|
||||
override fun getLookupString(): String {
|
||||
return lookupString
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ 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
|
||||
|
|
@ -18,7 +17,10 @@ interface LookupItem {
|
|||
val enabled: Boolean
|
||||
get() = true
|
||||
|
||||
fun createLookupElement(searchText: String = ""): LookupElement
|
||||
fun createLookupElement(
|
||||
searchText: String = "",
|
||||
searchTextProvider: (() -> String)? = null
|
||||
): LookupElement
|
||||
fun setPresentation(element: LookupElement, presentation: LookupElementPresentation)
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +29,8 @@ interface LookupGroupItem : LookupItem {
|
|||
}
|
||||
|
||||
interface DynamicLookupGroupItem : LookupGroupItem {
|
||||
suspend fun updateLookupList(lookup: LookupImpl, searchText: String)
|
||||
val minimumSearchTextLength: Int
|
||||
get() = 1
|
||||
}
|
||||
|
||||
interface LookupActionItem : LookupItem {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup
|
||||
|
||||
import com.intellij.codeInsight.completion.PrefixMatcher
|
||||
|
||||
class LookupItemPrefixMatcher(prefix: String) : PrefixMatcher(prefix) {
|
||||
|
||||
override fun prefixMatches(name: String): Boolean {
|
||||
return LookupMatchers.createMatcher(prefix).matchingDegree(name) != Int.MIN_VALUE
|
||||
}
|
||||
|
||||
override fun cloneWithPrefix(prefix: String): PrefixMatcher {
|
||||
return LookupItemPrefixMatcher(prefix)
|
||||
}
|
||||
|
||||
override fun matchingDegree(string: String): Int {
|
||||
return LookupMatchers.createMatcher(prefix).matchingDegree(string)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup
|
||||
|
||||
import com.intellij.psi.codeStyle.MinusculeMatcher
|
||||
import com.intellij.psi.codeStyle.NameUtil
|
||||
import com.intellij.util.text.matching.MatchingMode
|
||||
|
||||
object LookupMatchers {
|
||||
|
||||
fun createMatcher(searchText: String): MinusculeMatcher {
|
||||
val normalizedSearchText = searchText.trim()
|
||||
return if (normalizedSearchText.isEmpty()) {
|
||||
NameUtil.buildMatcher("*").build()
|
||||
} else {
|
||||
NameUtil.buildMatcherWithFallback(
|
||||
normalizedSearchText,
|
||||
"*$normalizedSearchText*",
|
||||
MatchingMode.IGNORE_CASE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,29 +5,33 @@ import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
|||
import com.intellij.codeInsight.lookup.impl.LookupImpl
|
||||
|
||||
object LookupUtil {
|
||||
private fun keepLookupOpenMatcher(prefix: String): PrefixMatcher =
|
||||
object : PrefixMatcher(prefix) {
|
||||
override fun prefixMatches(name: String): Boolean = true
|
||||
|
||||
fun addLookupItem(
|
||||
lookup: LookupImpl,
|
||||
lookupItem: LookupItem,
|
||||
priority: Double = 5.0,
|
||||
searchText: String = ""
|
||||
) {
|
||||
addLookupItems(lookup, listOf(lookupItem to priority), searchText)
|
||||
}
|
||||
override fun cloneWithPrefix(prefix: String): PrefixMatcher {
|
||||
return keepLookupOpenMatcher(prefix)
|
||||
}
|
||||
|
||||
override fun matchingDegree(string: String): Int = 0
|
||||
}
|
||||
|
||||
fun addLookupItems(
|
||||
lookup: LookupImpl,
|
||||
lookupItems: List<Pair<LookupItem, Double>>,
|
||||
searchText: String = ""
|
||||
searchText: String = "",
|
||||
searchTextProvider: (() -> String)? = null,
|
||||
matcherPrefix: String = searchText
|
||||
) {
|
||||
if (!lookup.isLookupDisposed) {
|
||||
val prefixMatcher = keepLookupOpenMatcher(matcherPrefix)
|
||||
lookupItems.forEach { (lookupItem, priority) ->
|
||||
lookup.addItem(
|
||||
PrioritizedLookupElement.withPriority(
|
||||
lookupItem.createLookupElement(searchText),
|
||||
lookupItem.createLookupElement(searchText, searchTextProvider),
|
||||
priority
|
||||
),
|
||||
PrefixMatcher.ALWAYS_TRUE
|
||||
prefixMatcher
|
||||
)
|
||||
}
|
||||
lookup.refreshUi(true, true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup
|
||||
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation
|
||||
|
||||
class StatusLookupItem(
|
||||
override val displayName: String,
|
||||
private val lookupString: String = displayName
|
||||
) : AbstractLookupItem() {
|
||||
|
||||
override val icon = null
|
||||
override val enabled = false
|
||||
|
||||
override fun setPresentation(
|
||||
element: LookupElement,
|
||||
presentation: LookupElementPresentation
|
||||
) {
|
||||
presentation.itemText = displayName
|
||||
presentation.isItemTextBold = false
|
||||
}
|
||||
|
||||
override fun getLookupString(): String {
|
||||
return lookupString
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import ee.carlrobert.codegpt.ui.textarea.header.tag.FolderTagDetails
|
|||
class FolderActionItem(
|
||||
private val project: Project,
|
||||
val folder: VirtualFile
|
||||
) : AbstractLookupActionItem(), InsertsDisplayNameLookupItem {
|
||||
) : AbstractLookupActionItem() {
|
||||
|
||||
override val displayName = folder.name
|
||||
override val icon = AllIcons.Nodes.Folder
|
||||
|
|
@ -33,4 +33,11 @@ class FolderActionItem(
|
|||
override fun execute(project: Project, userInputPanel: UserInputPanel) {
|
||||
userInputPanel.addTag(FolderTagDetails(folder))
|
||||
}
|
||||
|
||||
override fun getAdditionalLookupStrings(): Collection<String> {
|
||||
val projectRelativePath = project.guessProjectDir()?.let { projectDir ->
|
||||
VfsUtil.getRelativePath(folder, projectDir)
|
||||
}
|
||||
return listOfNotNull(folder.path, projectRelativePath)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea.lookup.action
|
||||
|
||||
interface InsertsDisplayNameLookupItem
|
||||
|
|
@ -11,14 +11,12 @@ import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
|||
import ee.carlrobert.codegpt.ui.textarea.FileSearchSource
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.AbstractLookupActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.InsertsDisplayNameLookupItem
|
||||
|
||||
class FileActionItem(
|
||||
private val project: Project,
|
||||
val file: VirtualFile,
|
||||
val source: FileSearchSource = FileSearchSource.OPEN
|
||||
) :
|
||||
AbstractLookupActionItem(), InsertsDisplayNameLookupItem {
|
||||
) : AbstractLookupActionItem() {
|
||||
|
||||
override val displayName = file.name
|
||||
override val icon = file.fileType.icon ?: AllIcons.FileTypes.Any_type
|
||||
|
|
@ -38,4 +36,11 @@ class FileActionItem(
|
|||
override fun execute(project: Project, userInputPanel: UserInputPanel) {
|
||||
userInputPanel.addTag(FileTagDetails(file))
|
||||
}
|
||||
|
||||
override fun getAdditionalLookupStrings(): Collection<String> {
|
||||
val projectRelativePath = project.guessProjectDir()?.let { projectDir ->
|
||||
VfsUtil.getRelativePath(file, projectDir)
|
||||
}
|
||||
return listOfNotNull(file.path, projectRelativePath)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import ee.carlrobert.codegpt.CodeGPTBundle
|
|||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.isHiddenFileOrInHiddenDirectory
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.AbstractLookupActionItem
|
||||
import javax.swing.Icon
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ class IncludeOpenFilesActionItem : AbstractLookupActionItem() {
|
|||
project.service<FileEditorManager>().openFiles
|
||||
.filter { openFile ->
|
||||
settingsService.isVirtualFileVisible(openFile) &&
|
||||
!openFile.isHiddenFileOrInHiddenDirectory() &&
|
||||
fileTags.none { it.virtualFile == openFile }
|
||||
}
|
||||
.forEach {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import com.intellij.openapi.project.Project
|
|||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.GitCommitTagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.AbstractLookupActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.InsertsDisplayNameLookupItem
|
||||
import git4idea.GitCommit
|
||||
|
||||
class GitCommitActionItem(
|
||||
private val gitCommit: GitCommit,
|
||||
) : AbstractLookupActionItem(), InsertsDisplayNameLookupItem {
|
||||
) : AbstractLookupActionItem() {
|
||||
|
||||
val description: String = gitCommit.id.asString().take(6)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,30 @@
|
|||
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.fileEditor.impl.EditorHistoryManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.psi.codeStyle.MinusculeMatcher
|
||||
import com.intellij.psi.codeStyle.NameUtil
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
|
||||
import ee.carlrobert.codegpt.ui.textarea.FileSearchCandidate
|
||||
import ee.carlrobert.codegpt.ui.textarea.FileSearchProvider
|
||||
import ee.carlrobert.codegpt.ui.textarea.FileSearchSource
|
||||
import ee.carlrobert.codegpt.ui.textarea.NativeFileSearchProvider
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
|
||||
import ee.carlrobert.codegpt.ui.textarea.isHiddenFileOrInHiddenDirectory
|
||||
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.LookupMatchers
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.IncludeOpenFilesActionItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FilesGroupItem(
|
||||
private val project: Project,
|
||||
|
|
@ -36,33 +32,28 @@ class FilesGroupItem(
|
|||
private val fileSearchProvider: FileSearchProvider = NativeFileSearchProvider(project)
|
||||
) : AbstractLookupGroupItem(), DynamicLookupGroupItem {
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
@Volatile
|
||||
private var cachedFolders: List<VirtualFile> = emptyList()
|
||||
@Volatile
|
||||
private var cachedFolderStructureModCount: Long = -1L
|
||||
@Volatile
|
||||
private var cachedFolderRootModCount: Long = -1L
|
||||
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.files.displayName")
|
||||
override val icon = AllIcons.FileTypes.Any_type
|
||||
|
||||
override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) {
|
||||
val lookupItems = getLookupItems(searchText)
|
||||
.filterIsInstance<FileActionItem>()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
lookupItems.forEach { actionItem ->
|
||||
runInEdt {
|
||||
LookupUtil.addLookupItem(lookup, actionItem, searchText = searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
|
||||
val normalizedSearchText = searchText.trim()
|
||||
val openFiles = getOpenFileCandidates(normalizedSearchText)
|
||||
val providerMatches = fileSearchProvider.search(normalizedSearchText, MAX_SEARCH_FILES)
|
||||
val providerFiles = readAction {
|
||||
val visibleProviderMatches = readAction {
|
||||
val projectFileIndex = project.service<ProjectFileIndex>()
|
||||
providerMatches.filter { candidate ->
|
||||
isVisibleProjectFile(candidate.file, projectFileIndex)
|
||||
isVisibleProjectItem(candidate.file, projectFileIndex)
|
||||
}
|
||||
}
|
||||
val providerFiles = visibleProviderMatches.filterNot { it.file.isDirectory }
|
||||
val folderItems = getFolderSuggestions(normalizedSearchText)
|
||||
|
||||
val orderedCandidates = if (normalizedSearchText.isEmpty()) {
|
||||
buildDefaultCandidates(
|
||||
|
|
@ -82,21 +73,19 @@ class FilesGroupItem(
|
|||
}.distinctBy { it.file.path }
|
||||
}
|
||||
|
||||
return orderedCandidates.toFileSuggestions()
|
||||
return orderedCandidates.toFileSuggestions(normalizedSearchText) + folderItems
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SEARCH_FILES = 200
|
||||
private const val MAX_DEFAULT_FILE_SUGGESTIONS = 25
|
||||
private const val MAX_SEARCH_FOLDERS = 200
|
||||
private const val MAX_DEFAULT_FOLDER_SUGGESTIONS = 10
|
||||
private const val MAX_OPEN_FILE_SUGGESTIONS = 15
|
||||
}
|
||||
|
||||
private fun createMatcher(searchText: String): MinusculeMatcher {
|
||||
return if (searchText.isEmpty()) {
|
||||
NameUtil.buildMatcher("*").build()
|
||||
} else {
|
||||
NameUtil.buildMatcher("*$searchText").build()
|
||||
}
|
||||
return LookupMatchers.createMatcher(searchText)
|
||||
}
|
||||
|
||||
private suspend fun getOpenFileCandidates(searchText: String): List<FileSearchCandidate> {
|
||||
|
|
@ -105,7 +94,7 @@ class FilesGroupItem(
|
|||
val projectFileIndex = project.service<ProjectFileIndex>()
|
||||
project.service<FileEditorManager>().openFiles
|
||||
.filter { file ->
|
||||
isVisibleProjectFile(file, projectFileIndex) && matcher.matches(file.name)
|
||||
matchingDegree(file, projectFileIndex, matcher) != Int.MIN_VALUE
|
||||
}
|
||||
.map { file ->
|
||||
FileSearchCandidate(
|
||||
|
|
@ -124,7 +113,7 @@ class FilesGroupItem(
|
|||
.asReversed()
|
||||
.asSequence()
|
||||
.filter { file ->
|
||||
isVisibleProjectFile(file, projectFileIndex) && matcher.matches(file.name)
|
||||
matchingDegree(file, projectFileIndex, matcher) != Int.MIN_VALUE
|
||||
}
|
||||
.take(MAX_SEARCH_FILES)
|
||||
.map { file ->
|
||||
|
|
@ -150,6 +139,44 @@ class FilesGroupItem(
|
|||
return prioritizedOpenFiles + recentBackfill
|
||||
}
|
||||
|
||||
private suspend fun getFolderSuggestions(searchText: String): List<FolderActionItem> {
|
||||
val matcher = createMatcher(searchText)
|
||||
val resultLimit = if (searchText.isEmpty()) {
|
||||
MAX_DEFAULT_FOLDER_SUGGESTIONS
|
||||
} else {
|
||||
MAX_SEARCH_FOLDERS
|
||||
}
|
||||
|
||||
return readAction {
|
||||
val projectFileIndex = project.service<ProjectFileIndex>()
|
||||
getProjectFolders(projectFileIndex)
|
||||
.asSequence()
|
||||
.filter { folder -> isVisibleProjectFolder(folder, projectFileIndex) }
|
||||
.mapNotNull { folder ->
|
||||
val matchingDegree = if (searchText.isEmpty()) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
maxOf(
|
||||
matcher.matchingDegree(folder.name),
|
||||
matcher.matchingDegree(folder.path)
|
||||
)
|
||||
}
|
||||
if (matchingDegree == Int.MIN_VALUE) {
|
||||
null
|
||||
} else {
|
||||
folder to matchingDegree
|
||||
}
|
||||
}
|
||||
.sortedWith(
|
||||
compareByDescending<Pair<VirtualFile, Int>> { it.second }
|
||||
.thenBy { it.first.path }
|
||||
)
|
||||
.take(resultLimit)
|
||||
.map { FolderActionItem(project, it.first) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsTag(file: VirtualFile): Boolean {
|
||||
return tagManager.containsTag(file)
|
||||
}
|
||||
|
|
@ -158,31 +185,91 @@ class FilesGroupItem(
|
|||
file: VirtualFile,
|
||||
projectFileIndex: ProjectFileIndex
|
||||
): Boolean {
|
||||
return !file.isDirectory &&
|
||||
projectFileIndex.isInContent(file) &&
|
||||
return isVisibleProjectItem(file, projectFileIndex) &&
|
||||
!file.isDirectory
|
||||
}
|
||||
|
||||
private fun isVisibleProjectFolder(
|
||||
file: VirtualFile,
|
||||
projectFileIndex: ProjectFileIndex
|
||||
): Boolean {
|
||||
return isProjectFolderCandidate(file, projectFileIndex) &&
|
||||
settingsService.isVirtualFileVisible(file) &&
|
||||
!file.isHiddenFileOrInHiddenDirectory() &&
|
||||
!containsTag(file)
|
||||
}
|
||||
|
||||
private fun Iterable<FileSearchCandidate>.toFileSuggestions(): List<LookupActionItem> {
|
||||
val selectedFileTags = getExistingTags(project, FileTagDetails::class.java)
|
||||
val fileItems = filter { candidate ->
|
||||
selectedFileTags.none { it.virtualFile == candidate.file }
|
||||
}.map { candidate ->
|
||||
private fun isVisibleProjectItem(
|
||||
file: VirtualFile,
|
||||
projectFileIndex: ProjectFileIndex
|
||||
): Boolean {
|
||||
return file.isValid &&
|
||||
projectFileIndex.isInContent(file) &&
|
||||
settingsService.isVirtualFileVisible(file) &&
|
||||
!file.isHiddenFileOrInHiddenDirectory() &&
|
||||
!containsTag(file)
|
||||
}
|
||||
|
||||
private fun isProjectFolderCandidate(
|
||||
file: VirtualFile,
|
||||
projectFileIndex: ProjectFileIndex
|
||||
): Boolean {
|
||||
return file.isValid &&
|
||||
file.isDirectory &&
|
||||
projectFileIndex.isInContent(file) &&
|
||||
!file.isHiddenFileOrInHiddenDirectory()
|
||||
}
|
||||
|
||||
private fun matchingDegree(
|
||||
file: VirtualFile,
|
||||
projectFileIndex: ProjectFileIndex,
|
||||
matcher: MinusculeMatcher
|
||||
): Int {
|
||||
if (!isVisibleProjectFile(file, projectFileIndex)) {
|
||||
return Int.MIN_VALUE
|
||||
}
|
||||
|
||||
return maxOf(
|
||||
matcher.matchingDegree(file.name),
|
||||
matcher.matchingDegree(file.path)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<FileSearchCandidate>.toFileSuggestions(searchText: String): List<LookupActionItem> {
|
||||
val fileItems = map { candidate ->
|
||||
FileActionItem(project, candidate.file, candidate.source)
|
||||
}
|
||||
|
||||
return listOf(IncludeOpenFilesActionItem()) + fileItems
|
||||
}
|
||||
val includeOpenFiles = if (searchText.isEmpty()) {
|
||||
listOf(IncludeOpenFilesActionItem())
|
||||
} else {
|
||||
val matcher = createMatcher(searchText)
|
||||
val item = IncludeOpenFilesActionItem()
|
||||
if (matcher.matchingDegree(item.displayName) != Int.MIN_VALUE) listOf(item) else emptyList()
|
||||
}
|
||||
|
||||
fun <T : TagDetails> getExistingTags(
|
||||
project: Project,
|
||||
tagClass: Class<T>
|
||||
): List<T> {
|
||||
return project.service<ChatToolWindowContentManager>()
|
||||
.tryFindActiveChatTabPanel()
|
||||
.map { it.selectedTags }
|
||||
.orElse(emptyList())
|
||||
.filterIsInstance(tagClass)
|
||||
return includeOpenFiles + fileItems
|
||||
}
|
||||
@Synchronized
|
||||
private fun getProjectFolders(projectFileIndex: ProjectFileIndex): List<VirtualFile> {
|
||||
val structureModCount = VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS.modificationCount
|
||||
val rootModCount = ProjectRootManager.getInstance(project).modificationCount
|
||||
if (cachedFolderStructureModCount == structureModCount &&
|
||||
cachedFolderRootModCount == rootModCount
|
||||
) {
|
||||
return cachedFolders
|
||||
}
|
||||
|
||||
val folders = mutableListOf<VirtualFile>()
|
||||
projectFileIndex.iterateContent { file ->
|
||||
if (isProjectFolderCandidate(file, projectFileIndex)) {
|
||||
folders += file
|
||||
}
|
||||
true
|
||||
}
|
||||
cachedFolders = folders
|
||||
cachedFolderStructureModCount = structureModCount
|
||||
cachedFolderRootModCount = rootModCount
|
||||
return folders
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
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 com.intellij.psi.codeStyle.NameUtil
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
|
||||
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.LookupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupUtil
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FoldersGroupItem(
|
||||
private val project: Project,
|
||||
private val tagManager: TagManager
|
||||
) : AbstractLookupGroupItem(), DynamicLookupGroupItem {
|
||||
private val settingsService = project.service<ProxyAISettingsService>()
|
||||
|
||||
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.folders.displayName")
|
||||
override val icon = AllIcons.Nodes.Folder
|
||||
|
||||
override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) {
|
||||
val folderSuggestions = getLookupItems(searchText)
|
||||
.filterIsInstance<FolderActionItem>()
|
||||
val existingPaths = lookup.items.mapNotNull { element ->
|
||||
(element.getUserData(LookupItem.KEY) as? FolderActionItem)?.folder?.path
|
||||
}.toSet()
|
||||
val newSuggestions = folderSuggestions
|
||||
.filterNot { it.folder.path in existingPaths }
|
||||
|
||||
if (newSuggestions.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
runInEdt {
|
||||
LookupUtil.addLookupItems(
|
||||
lookup,
|
||||
newSuggestions.map { it to 5.0 },
|
||||
searchText = searchText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
|
||||
val normalizedSearchText = searchText.trim()
|
||||
val matcher = NameUtil.buildMatcher("*$normalizedSearchText").build()
|
||||
|
||||
return getProjectFolders(project)
|
||||
.asSequence()
|
||||
.filter { folder ->
|
||||
settingsService.isVirtualFileVisible(folder) &&
|
||||
!tagManager.containsTag(folder)
|
||||
}
|
||||
.mapNotNull { folder ->
|
||||
if (normalizedSearchText.isEmpty()) {
|
||||
folder to Int.MAX_VALUE
|
||||
} else {
|
||||
val matchingDegree = maxOf(
|
||||
matcher.matchingDegree(folder.name),
|
||||
matcher.matchingDegree(folder.path)
|
||||
)
|
||||
if (matchingDegree == Int.MIN_VALUE) {
|
||||
null
|
||||
} else {
|
||||
folder to matchingDegree
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortedWith(
|
||||
compareByDescending<Pair<VirtualFile, Int>> { it.second }
|
||||
.thenBy { it.first.path }
|
||||
)
|
||||
.take(10)
|
||||
.map { FolderActionItem(project, it.first) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private suspend fun getProjectFolders(project: Project) = withContext(Dispatchers.IO) {
|
||||
buildProjectFolders(project)
|
||||
}
|
||||
|
||||
private fun buildProjectFolders(project: Project): List<VirtualFile> {
|
||||
val folders = mutableListOf<VirtualFile>()
|
||||
project.service<ProjectFileIndex>().iterateContent { file: VirtualFile ->
|
||||
if (file.isDirectory && !file.name.startsWith(".")) {
|
||||
folders.add(file)
|
||||
}
|
||||
true
|
||||
}
|
||||
return folders
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
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.action.git.GitCommitActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.git.IncludeCurrentChangesActionItem
|
||||
import ee.carlrobert.codegpt.util.GitUtil
|
||||
|
|
@ -21,26 +17,6 @@ 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 ->
|
||||
if (commit.id.asString().contains(searchText, true)
|
||||
|| commit.fullMessage.contains(searchText, true)
|
||||
) {
|
||||
runInEdt {
|
||||
LookupUtil.addLookupItem(
|
||||
lookup,
|
||||
GitCommitActionItem(commit),
|
||||
searchText = searchText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
GitUtil.getProjectRepository(project)?.let {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
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 ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.conversations.ConversationsState
|
||||
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.action.HistoryActionItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class HistoryGroupItem : AbstractLookupGroupItem(), DynamicLookupGroupItem {
|
||||
|
||||
|
|
@ -18,8 +13,6 @@ class HistoryGroupItem : AbstractLookupGroupItem(), DynamicLookupGroupItem {
|
|||
CodeGPTBundle.get("suggestionGroupItem.history.displayName")
|
||||
override val icon = AllIcons.Vcs.History
|
||||
|
||||
private val addedItems = mutableSetOf<String>()
|
||||
|
||||
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
|
||||
return ConversationsState.getInstance().conversations
|
||||
.sortedByDescending { it.updatedOn }
|
||||
|
|
@ -33,26 +26,4 @@ class HistoryGroupItem : AbstractLookupGroupItem(), DynamicLookupGroupItem {
|
|||
}
|
||||
.map { HistoryActionItem(it) }
|
||||
}
|
||||
|
||||
override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) {
|
||||
val filteredItems = getLookupItems(searchText)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
if (searchText.isEmpty()) {
|
||||
addedItems.clear()
|
||||
}
|
||||
|
||||
filteredItems.forEach { item ->
|
||||
val itemKey = item.displayName
|
||||
if (!addedItems.contains(itemKey)) {
|
||||
addedItems.add(itemKey)
|
||||
runInEdt {
|
||||
if (!lookup.isLookupDisposed) {
|
||||
LookupUtil.addLookupItem(lookup, item, searchText = searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,8 +347,7 @@ addDocumentation.popup.form.url.comment=Enter the full web address of the docume
|
|||
addDocumentation.popup.form.saveCheckbox.label=Save for future reference
|
||||
userMessagePanel.documentation.title=DOCUMENTATION
|
||||
userMessagePanel.persona.title=PERSONA
|
||||
suggestionGroupItem.files.displayName=Files
|
||||
suggestionGroupItem.folders.displayName=Folders
|
||||
suggestionGroupItem.files.displayName=Files and Folders
|
||||
suggestionGroupItem.personas.displayName=Personas
|
||||
suggestionGroupItem.history.displayName=History
|
||||
suggestionGroupItem.docs.displayName=Docs
|
||||
|
|
|
|||
|
|
@ -333,8 +333,7 @@ addDocumentation.popup.form.url.comment=\u8F93\u5165\u6587\u6863\u7684\u5B8C\u65
|
|||
addDocumentation.popup.form.saveCheckbox.label=\u4FDD\u5B58\u4EE5\u4F9B\u5C06\u6765\u53C2\u8003
|
||||
userMessagePanel.documentation.title=\u6587\u6863
|
||||
userMessagePanel.persona.title=\u89D2\u8272
|
||||
suggestionGroupItem.files.displayName=\u6587\u4EF6
|
||||
suggestionGroupItem.folders.displayName=\u6587\u4EF6\u5939
|
||||
suggestionGroupItem.files.displayName=\u6587\u4EF6\u548C\u6587\u4EF6\u5939
|
||||
suggestionGroupItem.personas.displayName=\u89D2\u8272
|
||||
suggestionGroupItem.history.displayName=\u5386\u53F2\u8BB0\u5F55
|
||||
suggestionGroupItem.docs.displayName=\u6587\u6863
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class AtLookupTokenTest {
|
||||
|
||||
@Test
|
||||
fun `test resolves lookup token before caret`() {
|
||||
val text = "before @first middle @second after"
|
||||
val caretOffset = text.indexOf(" after")
|
||||
|
||||
val token = AtLookupToken.from(text, caretOffset)
|
||||
|
||||
assertThat(token).isEqualTo(
|
||||
AtLookupToken(
|
||||
startOffset = text.indexOf("@second"),
|
||||
endOffset = caretOffset,
|
||||
searchText = "second"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ignores at symbols after caret`() {
|
||||
val text = "before @first middle @second"
|
||||
val caretOffset = text.indexOf(" middle")
|
||||
|
||||
val token = AtLookupToken.from(text, caretOffset)
|
||||
|
||||
assertThat(token).isEqualTo(
|
||||
AtLookupToken(
|
||||
startOffset = text.indexOf("@first"),
|
||||
endOffset = caretOffset,
|
||||
searchText = "first"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rejects token with whitespace`() {
|
||||
val text = "before @not a token"
|
||||
|
||||
val token = AtLookupToken.from(text, text.length)
|
||||
|
||||
assertThat(token).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
|
|||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.IncludeOpenFilesActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.group.FilesGroupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.group.FoldersGroupItem
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import testsupport.IntegrationTest
|
||||
|
|
@ -143,7 +142,7 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
|
|||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
val searchManager = SearchManager(project, TagManager())
|
||||
|
||||
val fileGroupSuggestions = runBlocking { filesGroupItem.getLookupItems("needle") }
|
||||
val fileGroupSuggestions = runBlocking { filesGroupItem.getLookupItems("") }
|
||||
val globalSearchResults = runBlocking { searchManager.performGlobalSearch("needle") }
|
||||
|
||||
assertThat(fileGroupSuggestions.first()).isInstanceOf(IncludeOpenFilesActionItem::class.java)
|
||||
|
|
@ -168,13 +167,39 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
|
|||
.anyMatch { it.endsWith("/app/src/test/Recent.kt") }
|
||||
}
|
||||
|
||||
fun `test folders group should not suggest ignored folders`() {
|
||||
fun `test files group typed search should include matching folders`() {
|
||||
val matchingFolder =
|
||||
myFixture.addFileToProject("app/docs/NeedleFolder/Visible.kt", "class Visible")
|
||||
.virtualFile.parent
|
||||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
|
||||
val folderSuggestions = runBlocking { filesGroupItem.getLookupItems("NeedleFolder") }
|
||||
.filterIsInstance<FolderActionItem>()
|
||||
|
||||
assertThat(folderSuggestions.map { it.folder.path })
|
||||
.contains(matchingFolder.path)
|
||||
}
|
||||
|
||||
fun `test files group typed search should include folders by path`() {
|
||||
val matchingFolder =
|
||||
myFixture.addFileToProject("app/docs/NestedFolder/Visible.kt", "class Visible")
|
||||
.virtualFile.parent
|
||||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
|
||||
val folderSuggestions = runBlocking { filesGroupItem.getLookupItems("app/docs/Nested") }
|
||||
.filterIsInstance<FolderActionItem>()
|
||||
|
||||
assertThat(folderSuggestions.map { it.folder.path })
|
||||
.contains(matchingFolder.path)
|
||||
}
|
||||
|
||||
fun `test files group should not suggest ignored folders`() {
|
||||
myFixture.addFileToProject("app/src/main/Hidden.kt", "class Hidden")
|
||||
myFixture.addFileToProject("app/src/test/Visible.kt", "class Visible")
|
||||
writeSettings(ignoreEntries = listOf("app/src/main/**"))
|
||||
val foldersGroupItem = FoldersGroupItem(project, TagManager())
|
||||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
|
||||
val folderSuggestions = runBlocking { foldersGroupItem.getLookupItems("src") }
|
||||
val folderSuggestions = runBlocking { filesGroupItem.getLookupItems("src") }
|
||||
.filterIsInstance<FolderActionItem>()
|
||||
.map { it.folder.path }
|
||||
|
||||
|
|
@ -183,19 +208,39 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
|
|||
.anyMatch { it.endsWith("/app/src/test") }
|
||||
}
|
||||
|
||||
fun `test folders group should include folders added after the first lookup`() {
|
||||
myFixture.addFileToProject("app/src/main/Existing.kt", "class Existing")
|
||||
val foldersGroupItem = FoldersGroupItem(project, TagManager())
|
||||
fun `test files group should not suggest dotfiles or files inside dot folders`() {
|
||||
val dotFile = myFixture.addFileToProject(".env", "SECRET=value").virtualFile
|
||||
val dotFolderFile =
|
||||
myFixture.addFileToProject(".github/workflows/build.yml", "name: build").virtualFile
|
||||
val visibleFile =
|
||||
myFixture.addFileToProject("app/src/test/VisibleEnv.kt", "class VisibleEnv")
|
||||
.virtualFile
|
||||
openFiles(dotFile, dotFolderFile, visibleFile)
|
||||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
|
||||
runBlocking { foldersGroupItem.getLookupItems("src") }
|
||||
myFixture.addFileToProject("app/docs/NewDoc.kt", "class NewDoc")
|
||||
val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("env") }
|
||||
.filterIsInstance<FileActionItem>()
|
||||
.map { it.file.path }
|
||||
|
||||
val folderSuggestions = runBlocking { foldersGroupItem.getLookupItems("docs") }
|
||||
assertThat(fileSuggestions)
|
||||
.noneMatch { it.endsWith("/.env") }
|
||||
.noneMatch { it.contains("/.github/") }
|
||||
.anyMatch { it.endsWith("/app/src/test/VisibleEnv.kt") }
|
||||
}
|
||||
|
||||
fun `test files group should not suggest dot folders or their child folders`() {
|
||||
myFixture.addFileToProject(".github/workflows/build.yml", "name: build")
|
||||
myFixture.addFileToProject("app/github/Visible.kt", "class Visible")
|
||||
val filesGroupItem = FilesGroupItem(project, TagManager())
|
||||
|
||||
val folderSuggestions = runBlocking { filesGroupItem.getLookupItems("github") }
|
||||
.filterIsInstance<FolderActionItem>()
|
||||
.map { it.folder.path }
|
||||
|
||||
assertThat(folderSuggestions)
|
||||
.anyMatch { it.endsWith("/app/docs") }
|
||||
.noneMatch { it.endsWith("/.github") }
|
||||
.noneMatch { it.contains("/.github/") }
|
||||
.anyMatch { it.endsWith("/app/github") }
|
||||
}
|
||||
|
||||
fun `test merge results should keep folders with the same display name`() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package ee.carlrobert.codegpt.ui.textarea
|
||||
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation
|
||||
import com.intellij.openapi.project.Project
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.AbstractLookupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LoadingLookupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.StatusLookupItem
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class PromptTextFieldLookupItemsTest {
|
||||
|
||||
@Test
|
||||
fun `calculating empty results returns loading row`() {
|
||||
val items = PromptTextField.toSearchLookupItems(
|
||||
results = emptyList(),
|
||||
searchText = "abc",
|
||||
isCalculating = true
|
||||
)
|
||||
|
||||
assertThat(items).hasSize(1)
|
||||
assertThat(items.single()).isInstanceOf(LoadingLookupItem::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `done empty results returns no results row`() {
|
||||
val items = PromptTextField.toSearchLookupItems(
|
||||
results = emptyList(),
|
||||
searchText = "abc",
|
||||
isCalculating = false
|
||||
)
|
||||
|
||||
assertThat(items).hasSize(1)
|
||||
assertThat(items.single()).isInstanceOf(StatusLookupItem::class.java)
|
||||
assertThat(items.single().displayName)
|
||||
.isEqualTo(PromptTextFieldLookupManager.EMPTY_RESULTS_TEXT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pending empty results before loading reveal returns no rows`() {
|
||||
val items = PromptTextField.toSearchLookupItems(
|
||||
results = emptyList(),
|
||||
searchText = "abc",
|
||||
isCalculating = false,
|
||||
showEmptyStatus = false
|
||||
)
|
||||
|
||||
assertThat(items).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculating real results appends loading row`() {
|
||||
val result = TestLookupActionItem("abc.kt")
|
||||
|
||||
val items = PromptTextField.toSearchLookupItems(
|
||||
results = listOf(result),
|
||||
searchText = "abc",
|
||||
isCalculating = true
|
||||
)
|
||||
|
||||
assertThat(items).containsExactly(result, items.last())
|
||||
assertThat(items.last()).isInstanceOf(LoadingLookupItem::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculating empty results keeps previous visible rows with loading row`() {
|
||||
val previous = TestLookupActionItem("abc.kt")
|
||||
|
||||
val items = PromptTextField.toSearchLookupItems(
|
||||
results = emptyList(),
|
||||
searchText = "abcd",
|
||||
isCalculating = true,
|
||||
previousVisibleLookupItems = listOf(previous)
|
||||
)
|
||||
|
||||
assertThat(items).containsExactly(previous, items.last())
|
||||
assertThat(items.last()).isInstanceOf(LoadingLookupItem::class.java)
|
||||
}
|
||||
|
||||
private class TestLookupActionItem(
|
||||
override val displayName: String
|
||||
) : AbstractLookupItem(), LookupActionItem {
|
||||
|
||||
override val icon = null
|
||||
|
||||
override fun execute(project: Project, userInputPanel: UserInputPanel) = Unit
|
||||
|
||||
override fun setPresentation(
|
||||
element: LookupElement,
|
||||
presentation: LookupElementPresentation
|
||||
) {
|
||||
presentation.itemText = displayName
|
||||
}
|
||||
|
||||
override fun getLookupString(): String {
|
||||
return displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue