feat: Added code structure analysis with depth configuration and improved tag handling (#1045)

* feat: add Kotlin inferred type analyzer

* feat: implemented a queue with support for maximum crawl depth

* feat: added the depth of analysis setting to end the chat

* feat: added the depth of analysis setting to code completion

* feat: add tag for code analysis

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

* feat: changed priority of EditorTagDetails and FileTagDetails

If we added a file when opening a tab, and then added the same file through the "Include files in Prompt..." menu, it will not be in the selected state.

---------

Co-authored-by: alexander.korovin <alexander.korovin@vk.team>
Co-authored-by: Carl-Robert Linnupuu <carlrobertoh@gmail.com>
This commit is contained in:
Violine 2025-07-04 17:53:12 +03:00 committed by Carl-Robert Linnupuu
parent aef332e486
commit 2ead45efde
21 changed files with 318 additions and 52 deletions

View file

@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.toolwindow.chat.structure.data
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
@ -13,6 +14,7 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.util.io.await
import ee.carlrobert.codegpt.psistructure.PsiStructureProvider
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.configuration.ConfigurationStateListener
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers
@ -54,10 +56,29 @@ class PsiStructureRepository(
}
override fun onTagSelectionChanged(tag: TagDetails) {
(tag as? CodeAnalyzeTagDetails)?.let {
handleCodeAnalyzeTag(it)
}
val tags = tagManager.getTags().getPsiAnalyzedTags()
update(tags)
}
private fun handleCodeAnalyzeTag(tag: CodeAnalyzeTagDetails) {
if (!tag.selected) {
_structureState.value = PsiStructureState.Content(emptySet(), emptySet())
service<ConfigurationSettings>().state
.chatCompletionSettings
.psiStructureEnabled = false
disable()
} else {
service<ConfigurationSettings>().state
.chatCompletionSettings
.psiStructureEnabled = true
enable()
}
}
private fun updatePsiStructureIfNeeded() {
val tags = tagManager.getTags().getPsiAnalyzedTags()
if (isNeedUpdatePsiStructure(tags)) {
@ -99,6 +120,8 @@ class PsiStructureRepository(
}
}
private var analyzePsiDepth = Int.MAX_VALUE
init {
Disposer.register(parentDisposable, coroutineScope)
tagManager.addListener(tagsListener)
@ -113,7 +136,20 @@ class PsiStructureRepository(
connection.subscribe(
ConfigurationStateListener.TOPIC,
ConfigurationStateListener { newState ->
tagManager.getTags()
.filterIsInstance<CodeAnalyzeTagDetails>()
.forEach { tagManager.remove(it) }
if (tagManager.getTags().any { it is EditorTagDetails || it is FileTagDetails }) {
tagManager.addTag(
CodeAnalyzeTagDetails().apply {
selected = newState.chatCompletionSettings.psiStructureEnabled
}
)
}
if (newState.chatCompletionSettings.psiStructureEnabled) {
analyzePsiDepth = newState.chatCompletionSettings.psiStructureAnalyzeDepth
enable()
} else {
disable()
@ -164,7 +200,7 @@ class PsiStructureRepository(
.await()
val virtualFilesToRemoveFromStructure = tags.getExcludedVirtualFiles()
val result = psiStructureProvider.get(psiFiles)
val result = psiStructureProvider.get(psiFiles, analyzePsiDepth)
.filter { classStructure ->
!virtualFilesToRemoveFromStructure.contains(classStructure.virtualFile)
}
@ -197,8 +233,9 @@ class PsiStructureRepository(
is EmptyTagDetails -> null
is WebTagDetails -> null
is ImageTagDetails -> null
is CodeAnalyzeTagDetails -> null
}
virtualFile?.takeIf { it.isValid && it.exists()}
}
}
@ -223,6 +260,7 @@ class PsiStructureRepository(
is EmptyTagDetails -> false
is WebTagDetails -> false
is ImageTagDetails -> false
is CodeAnalyzeTagDetails -> false
}
}
.toSet()
@ -249,8 +287,9 @@ class PsiStructureRepository(
is EmptyTagDetails -> null
is WebTagDetails -> null
is ImageTagDetails -> null
is CodeAnalyzeTagDetails -> null
}
virtualFile?.takeIf { it.isValid && it.exists()}
}
}

View file

@ -63,6 +63,8 @@ public class TotalTokensPanel extends JPanel {
if (ConfigurationSettings.getState().getChatCompletionSettings()
.getPsiStructureEnabled()) {
updatePsiTokenCount(psiTokens);
} else {
updatePsiTokenCount(0);
}
return Unit.INSTANCE;
}

View file

@ -40,7 +40,8 @@ object InfillRequestUtil {
}
if (service<ConfigurationSettings>().state.codeCompletionSettings.collectDependencyStructure) {
val psiStructure = PsiStructureProvider().get(listOf(request.file))
val depth = service<ConfigurationSettings>().state.codeCompletionSettings.psiStructureAnalyzeDepth
val psiStructure = PsiStructureProvider().get(listOf(request.file), depth)
if (psiStructure.isNotEmpty()) {
infillRequestBuilder.addDependenciesStructure(psiStructure)
infillRequestBuilder.addRepositoryName(psiStructure.first().repositoryName)

View file

@ -17,26 +17,15 @@ import ee.carlrobert.codegpt.psistructure.models.MethodStructure
import ee.carlrobert.codegpt.psistructure.models.ParameterInfo
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtConstructor
import org.jetbrains.kotlin.psi.KtEnumEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtVariableDeclaration
import org.jetbrains.kotlin.psi.*
class KotlinFileAnalyzer(
private val psiFileQueue: PsiFileQueue,
private val psiFileQueue: PsiFileDepthQueue,
private val ktFile: KtFile,
) {
private val psiManager = PsiManager.getInstance(ktFile.project)
private val kotlinPropertyAnalyzer = KotlinPropertyAnalyzer()
private val filePackageTypes: Map<String, String> by lazy {
val types = mutableListOf<String>()
@ -259,7 +248,7 @@ class KotlinFileAnalyzer(
}
private fun analyzeProperty(property: KtProperty): FieldStructure {
val type = property.typeReference?.text ?: "TypeUnknown"
val type = property.typeReference?.text ?: kotlinPropertyAnalyzer.resolveInferredType(property)
val resolvedType = resolveType(type)
val modifierList = getModifiers(property)
return FieldStructure(property.name ?: "", resolvedType, modifierList)
@ -335,7 +324,7 @@ class KotlinFileAnalyzer(
}
foundKtFiles.forEach { psiFile ->
psiFileQueue.put(psiFile)
psiFileQueue.put(psiFile, ktFile.name)
}
}
}

View file

@ -0,0 +1,111 @@
package ee.carlrobert.codegpt.psistructure
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
class KotlinPropertyAnalyzer {
fun resolveInferredType(property: KtProperty): String {
return try {
val initializer = property.initializer
if (initializer != null) {
val bindingContext = initializer.analyze()
val type = bindingContext.getType(initializer)?.toString()
if (type != null) {
return type
}
}
val delExpression = property.delegate?.expression
if (delExpression != null) {
val delegatedType = resolveDelegatedPropertyType(property)
if (delegatedType != TYPE_UNKNOWN) {
return delegatedType
}
}
val getterType = resolvePropertyWithGetter(property)
if (getterType != TYPE_UNKNOWN) {
return getterType
}
val expressionType = resolveExpressionType(property)
if (expressionType != TYPE_UNKNOWN) {
return expressionType
}
TYPE_UNKNOWN
} catch (e: Exception) {
TYPE_UNKNOWN
}
}
private fun resolveDelegatedPropertyType(property: KtProperty): String {
return property.delegate?.expression?.let { delegateExpr ->
when (val initializer = getDelegateInitializer(delegateExpr)) {
is KtLambdaExpression -> resolveLambdaReturnType(initializer)
else -> resolveExpressionType(initializer)
}
} ?: TYPE_UNKNOWN
}
private fun getDelegateInitializer(expr: KtExpression): KtExpression? {
return when (expr) {
is KtCallExpression -> expr.lambdaArguments.firstOrNull()?.getLambdaExpression()
is KtBinaryExpression -> expr.right?.let(::getDelegateInitializer)
else -> null
}
}
private fun resolveLambdaReturnType(lambda: KtLambdaExpression): String {
return try {
val lastExpr = lambda.bodyExpression?.statements?.lastOrNull()
val bindingContext = lambda.analyze()
lastExpr?.let { bindingContext.getType(it)?.toString() } ?: TYPE_UNKNOWN
} catch (e: Exception) {
TYPE_UNKNOWN
}
}
private fun resolvePropertyWithGetter(property: KtProperty): String {
return property.getter?.bodyExpression?.let { expr ->
try {
val bindingContext = expr.analyze()
bindingContext.getType(expr)?.toString() ?: TYPE_UNKNOWN
} catch (e: Exception) {
TYPE_UNKNOWN
}
} ?: TYPE_UNKNOWN
}
private fun resolveExpressionType(expression: KtExpression?): String {
if (expression == null) return TYPE_UNKNOWN
if (expression is KtDotQualifiedExpression) {
return resolveQualifiedChain(expression)
}
return try {
val bindingContext = expression.analyze()
val ktType = bindingContext.getType(expression)
ktType?.toString() ?: TYPE_UNKNOWN
} catch (e: Exception) {
TYPE_UNKNOWN
}
}
private fun resolveQualifiedChain(expr: KtDotQualifiedExpression): String {
return buildString {
var currentExpr: KtExpression? = expr
while (currentExpr is KtDotQualifiedExpression) {
val selector = currentExpr.selectorExpression?.text ?: break
append(".").append(selector)
currentExpr = currentExpr.receiverExpression
}
val rootType = currentExpr?.let(::resolveExpressionType) ?: TYPE_UNKNOWN
replaceFirst(Regex("."), rootType)
}
}
}
private const val TYPE_UNKNOWN = "TypeUnknown"

View file

@ -0,0 +1,8 @@
package ee.carlrobert.codegpt.psistructure
import com.intellij.psi.PsiFile
data class PsiDepthFile(
val psiFile: PsiFile,
val depth: Int,
)

View file

@ -0,0 +1,37 @@
package ee.carlrobert.codegpt.psistructure
import com.intellij.psi.PsiFile
class PsiFileDepthQueue(
initial: List<PsiFile>,
private val maxDepth: Int = -1,
) {
private val psiFiles = mutableSetOf<PsiDepthFile>().apply {
addAll(initial.map { PsiDepthFile(it, 0) })
}
private val queue = ArrayDeque(initial.map { PsiDepthFile(it, 0) })
@Synchronized
fun pop(): PsiFile? {
while (queue.isNotEmpty()) {
val first = queue.first()
if (maxDepth == -1 || first.depth <= maxDepth) {
return queue.removeFirst().psiFile
} else {
queue.removeFirst()
}
}
return null
}
@Synchronized
fun put(psiFile: PsiFile, baseFileName: String) {
if (psiFiles.any { it.psiFile.name == psiFile.name }) return
val baseFileDepth = psiFiles.find { it.psiFile.name == baseFileName }?.depth ?: 0
val newItem = PsiDepthFile(psiFile, baseFileDepth + 1)
queue.add(newItem)
psiFiles.add(newItem)
}
}

View file

@ -1,19 +0,0 @@
package ee.carlrobert.codegpt.psistructure
import com.intellij.psi.PsiFile
class PsiFileQueue(
initial: List<PsiFile>
) {
private val queue = ArrayDeque(initial)
@Synchronized
fun pop(): PsiFile? =
queue.removeFirstOrNull()
@Synchronized
fun put(psiFile: PsiFile) {
queue.add(psiFile)
}
}

View file

@ -15,7 +15,10 @@ import kotlin.coroutines.cancellation.CancellationException
class PsiStructureProvider {
suspend fun get(psiFiles: List<PsiFile>): Set<ClassStructure> {
suspend fun get(
psiFiles: List<PsiFile>,
analyzeDepth: Int,
): Set<ClassStructure> {
var result: Set<ClassStructure>? = null
var attempts = 0
val maxAttempts = 5
@ -35,17 +38,23 @@ class PsiStructureProvider {
val future = ReadAction.nonBlocking<Set<ClassStructure>> {
val classStructureSet = mutableSetOf<ClassStructure>()
val processedPsiFiles = mutableSetOf<PsiFile?>()
val psiFileQueue = PsiFileQueue(psiFiles)
val psiFileDepthQueue = PsiFileDepthQueue(psiFiles, analyzeDepth)
while (true) {
coroutineContext.ensureActive()
val psiFile = psiFileQueue.pop()
val psiFile = psiFileDepthQueue.pop()
when {
processedPsiFiles.contains(psiFile) -> Unit
kotlinFileAnalyzerAvailable && psiFile is KtFile -> {
classStructureSet.addAll(KotlinFileAnalyzer(psiFileQueue, psiFile).analyze())
classStructureSet.addAll(
KotlinFileAnalyzer(
psiFileQueue = psiFileDepthQueue,
ktFile = psiFile,
).analyze()
)
processedPsiFiles.add(psiFile)
}

View file

@ -2,7 +2,10 @@ package ee.carlrobert.codegpt.settings.configuration
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.PortField
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.fields.IntegerField
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
@ -19,6 +22,10 @@ class ChatCompletionConfigurationForm {
service<ConfigurationSettings>().state.chatCompletionSettings.psiStructureEnabled
)
private val psiStructureAnalyzeDepthField = PortField().apply {
number = service<ConfigurationSettings>().state.chatCompletionSettings.psiStructureAnalyzeDepth
}
fun createPanel(): DialogPanel {
return panel {
row {
@ -29,18 +36,27 @@ class ChatCompletionConfigurationForm {
cell(psiStructureCheckBox)
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.psiStructure.description"))
}
row {
label(
CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title"),
)
cell(psiStructureAnalyzeDepthField)
.comment(CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment"))
}
}.withBorder(JBUI.Borders.emptyLeft(16))
}
fun resetForm(prevState: ChatCompletionSettingsState) {
editorContextTagCheckBox.isSelected = prevState.editorContextTagEnabled
psiStructureCheckBox.isSelected = prevState.psiStructureEnabled
psiStructureAnalyzeDepthField.number = prevState.psiStructureAnalyzeDepth
}
fun getFormState(): ChatCompletionSettingsState {
return ChatCompletionSettingsState().apply {
this.editorContextTagEnabled = editorContextTagCheckBox.isSelected
this.psiStructureEnabled = psiStructureCheckBox.isSelected
this.psiStructureAnalyzeDepth = psiStructureAnalyzeDepthField.number
}
}
}

View file

@ -2,7 +2,9 @@ package ee.carlrobert.codegpt.settings.configuration
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.PortField
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.fields.IntegerField
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
@ -22,6 +24,10 @@ class CodeCompletionConfigurationForm {
service<ConfigurationSettings>().state.codeCompletionSettings.collectDependencyStructure
)
private val psiStructureAnalyzeDepthField = PortField().apply {
number = service<ConfigurationSettings>().state.codeCompletionSettings.psiStructureAnalyzeDepth
}
fun createPanel(): DialogPanel {
return panel {
row {
@ -36,12 +42,20 @@ class CodeCompletionConfigurationForm {
cell(collectDependencyStructureBox)
.comment(CodeGPTBundle.get("configurationConfigurable.section.codeCompletion.collectDependencyStructure.description"))
}
row {
label(
CodeGPTBundle.get("configurationConfigurable.section.codeCompletion.analyzeDepth.title"),
)
cell(psiStructureAnalyzeDepthField)
.comment(CodeGPTBundle.get("configurationConfigurable.section.codeCompletion.analyzeDepth.comment"))
}
}.withBorder(JBUI.Borders.emptyLeft(16))
}
fun resetForm(prevState: CodeCompletionSettingsState) {
treeSitterProcessingCheckBox.isSelected = prevState.treeSitterProcessingEnabled
gitDiffCheckBox.isSelected = prevState.gitDiffEnabled
psiStructureAnalyzeDepthField.number = prevState.psiStructureAnalyzeDepth
}
fun getFormState(): CodeCompletionSettingsState {
@ -49,6 +63,7 @@ class CodeCompletionConfigurationForm {
this.treeSitterProcessingEnabled = treeSitterProcessingCheckBox.isSelected
this.gitDiffEnabled = gitDiffCheckBox.isSelected
this.collectDependencyStructure = collectDependencyStructureBox.isSelected
this.psiStructureAnalyzeDepth = psiStructureAnalyzeDepthField.number
}
}
}

View file

@ -43,6 +43,7 @@ class ConfigurationSettingsState : BaseState() {
class ChatCompletionSettingsState : BaseState() {
var editorContextTagEnabled by property(true)
var psiStructureEnabled by property(true)
var psiStructureAnalyzeDepth by property(3)
}
class CodeCompletionSettingsState : BaseState() {
@ -50,4 +51,5 @@ class CodeCompletionSettingsState : BaseState() {
var gitDiffEnabled by property(true)
var collectDependencyStructure by property(true)
var contextAwareEnabled by property(false)
var psiStructureAnalyzeDepth by property(2)
}

View file

@ -95,6 +95,9 @@ class PromptTextField(
},
onWebActionSelected = { webAction ->
onLookupAdded(webAction)
},
onCodeAnalyzeSelected = { codeAnalyzeAction ->
onLookupAdded(codeAnalyzeAction)
}
)
}
@ -322,6 +325,7 @@ class PromptTextField(
)
},
onWebActionSelected = { webAction -> onLookupAdded(webAction) },
onCodeAnalyzeSelected = { codeAnalyzeAction -> onLookupAdded(codeAnalyzeAction) },
searchText = ""
)
}

View file

@ -15,6 +15,7 @@ import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.CodeAnalyzeActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem
@ -43,6 +44,7 @@ class PromptTextFieldLookupManager(
lookupElements: Array<LookupElement>,
onGroupSelected: (group: LookupGroupItem, searchText: String) -> Unit,
onWebActionSelected: (WebActionItem) -> Unit,
onCodeAnalyzeSelected: (CodeAnalyzeActionItem) -> Unit,
searchText: String = ""
): LookupImpl {
val lookup = createLookup(editor, lookupElements, "")
@ -55,6 +57,7 @@ class PromptTextFieldLookupManager(
when (suggestion) {
is WebActionItem -> onWebActionSelected(suggestion)
is CodeAnalyzeActionItem -> onCodeAnalyzeSelected(suggestion)
is LookupGroupItem -> onGroupSelected(suggestion, searchText)
is LookupActionItem -> onLookupAdded(suggestion)
}

View file

@ -7,6 +7,7 @@ import com.intellij.psi.codeStyle.NameUtil
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.CodeAnalyzeActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.WebActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.ImageActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.group.*
@ -33,6 +34,7 @@ class SearchManager(
HistoryGroupItem(),
PersonasGroupItem(tagManager),
DocsGroupItem(tagManager),
CodeAnalyzeActionItem(tagManager),
MCPGroupItem(),
WebActionItem(tagManager),
ImageActionItem(project, tagManager)

View file

@ -8,12 +8,14 @@ internal class TagDetailsComparator : Comparator<TagDetails> {
}
private fun getPriority(tag: TagDetails): Int {
if (!tag.selected) {
if (!tag.selected && tag !is CodeAnalyzeTagDetails) {
return Int.MAX_VALUE
}
return when (tag) {
is CodeAnalyzeTagDetails,
is EditorSelectionTagDetails -> 0
is SelectionTagDetails -> 5
is DocumentationTagDetails,
is PersonaTagDetails,

View file

@ -32,6 +32,7 @@ object TagProcessorFactory {
is EditorTagDetails -> EditorTagProcessor(tagDetails)
is ImageTagDetails -> ImageTagProcessor(tagDetails)
is EmptyTagDetails -> TagProcessor { _, _ -> }
is CodeAnalyzeTagDetails -> TagProcessor { _, _ -> }
}
}
}

View file

@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.ui.textarea.header
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.SelectionModel
@ -17,6 +18,7 @@ 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.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
import ee.carlrobert.codegpt.ui.WrapLayout
import ee.carlrobert.codegpt.ui.textarea.PromptTextField
@ -107,18 +109,18 @@ class UserInputHeaderPanel(
val allTags = tagManager.getTags()
val editorVirtualFilesSet = allTags
.filterIsInstance<EditorTagDetails>()
val filesVirtualFilesSet = allTags
.filterIsInstance<FileTagDetails>()
.map { it.virtualFile }
.toSet()
/**
* Filter the tags collection to prioritize EditorTagDetails over FileTagDetails
* Keep all tags except FileTagDetails that have a corresponding EditorTagDetails
* Filter the tags collection to prioritize FileTagDetails over EditorTagDetails
* Keep all tags except EditorTagDetails that have a corresponding FileTagDetails
*/
val tags = allTags.filter { tag ->
if (tag is FileTagDetails) {
!editorVirtualFilesSet.contains(tag.virtualFile)
if (tag is EditorTagDetails) {
!filesVirtualFilesSet.contains(tag.virtualFile)
} else {
true
}
@ -172,6 +174,14 @@ class UserInputHeaderPanel(
tagManager.addTag(EditorTagDetails(selectedFile))
}
val psiStructureEnabled = service<ConfigurationSettings>().state
.chatCompletionSettings
.psiStructureEnabled
tagManager.addTag(
CodeAnalyzeTagDetails().apply { selected = psiStructureEnabled }
)
EditorUtil.getOpenLocalFiles(project)
.filterNot { it == selectedFile }
.take(INITIAL_VISIBLE_FILES)

View file

@ -121,7 +121,7 @@ data class FolderTagDetails(var folder: VirtualFile) :
class WebTagDetails : TagDetails("Web", AllIcons.General.Web)
data class ImageTagDetails(val imagePath: String) :
data class ImageTagDetails(val imagePath: String) :
TagDetails(imagePath.substringAfterLast('/'), AllIcons.FileTypes.Image)
data class HistoryTagDetails(
@ -129,4 +129,6 @@ data class HistoryTagDetails(
val title: String,
) : TagDetails(title, AllIcons.General.Balloon)
class EmptyTagDetails : TagDetails("")
class EmptyTagDetails : TagDetails("")
class CodeAnalyzeTagDetails : TagDetails("Code Analyze", AllIcons.Actions.DependencyAnalyzer)

View file

@ -0,0 +1,27 @@
package ee.carlrobert.codegpt.ui.textarea.lookup.action
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
import ee.carlrobert.codegpt.ui.textarea.header.tag.CodeAnalyzeTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.EditorTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagDetails
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
class CodeAnalyzeActionItem(
private val tagManager: TagManager
) : AbstractLookupActionItem() {
override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.codeAnalyze.displayName")
override val icon = AllIcons.Actions.DependencyAnalyzer
override val enabled: Boolean
get() = tagManager.getTags().none { it is CodeAnalyzeTagDetails } &&
tagManager.getTags().any { it is FileTagDetails || it is EditorTagDetails }
override fun execute(project: Project, userInputPanel: UserInputPanel) {
userInputPanel.addTag(CodeAnalyzeTagDetails())
}
}

View file

@ -133,6 +133,8 @@ configurationConfigurable.section.codeCompletion.postProcess.title=Enable tree-s
configurationConfigurable.section.codeCompletion.postProcess.description=If checked, the completion will be post-processed using the tree-sitter parser.
configurationConfigurable.section.codeCompletion.gitDiff.title=Enable git diff context
configurationConfigurable.section.codeCompletion.collectDependencyStructure.title=Enable dependency analyzer
configurationConfigurable.section.codeCompletion.analyzeDepth.title=Code analyze depth:
configurationConfigurable.section.codeCompletion.analyzeDepth.comment=The parameter limits the depth of the PSI structure traversal. Currently, it is implemented only for the Kotlin language.
configurationConfigurable.section.codeCompletion.collectDependencyStructure.description=Enabling the setting allows the plugin to collect the dependency structure, which increases the accuracy of the proposed data, but consumes more tokens per request. Currently, it is implemented only for the Kotlin language.
configurationConfigurable.section.codeCompletion.gitDiff.description=If checked, the user's most recent unstaged git diff will be included when requesting completion.
configurationConfigurable.section.chatCompletion.title=Chat Completion
@ -141,6 +143,8 @@ configurationConfigurable.section.chatCompletion.retryOnFailedDiffSearch.descrip
configurationConfigurable.section.chatCompletion.editorContextTag.title=Enable automatic file tagging
configurationConfigurable.section.chatCompletion.editorContextTag.description=If enabled, the content from open editor files will be automatically included with each message you send.
configurationConfigurable.section.chatCompletion.psiStructure.title=Enable dependency structure analysis of attached files.
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.title=Code analyze depth:
configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comment=The parameter limits the depth of the PSI structure traversal. Currently, it is implemented only for the Kotlin language.
configurationConfigurable.section.chatCompletion.psiStructure.description=If enabled, the class structure that is present in the imports of the attached files will be added in the context of the dialog. A structure refers to the source code in files that include constructors, fields, and methods, with all modifiers, arguments, and return types, but without an implementation. The implementation of dependencies is intentionally excluded in order to find a balance between a high-quality chat context and saving tokens.
settingsConfigurable.service.llama.topK.label=Top K:
settingsConfigurable.service.llama.topK.comment=Limit the next token selection to the K most probable tokens (default: 40)
@ -314,6 +318,7 @@ suggestionGroupItem.history.displayName=History
suggestionGroupItem.docs.displayName=Docs
suggestionGroupItem.git.displayName=Git
suggestionGroupItem.mcp.displayName=MCP (soon)
suggestionGroupItem.codeAnalyze.displayName=Code Analyze
suggestionActionItem.attachImage.displayName=Image
suggestionActionItem.attachImage.description=Select an image file to attach
suggestionActionItem.webSearch.displayName=Web