From 2ead45efdedde75939bdf6029ca32bbc8493cc43 Mon Sep 17 00:00:00 2001 From: Violine <3253340@gmail.com> Date: Fri, 4 Jul 2025 17:53:12 +0300 Subject: [PATCH] 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 Co-authored-by: Carl-Robert Linnupuu --- .../structure/data/PsiStructureRepository.kt | 45 ++++++- .../chat/ui/textarea/TotalTokensPanel.java | 2 + .../codecompletions/InfillRequestUtil.kt | 3 +- .../psistructure/KotlinFileAnalyzer.kt | 21 +--- .../psistructure/KotlinPropertyAnalyzer.kt | 111 ++++++++++++++++++ .../codegpt/psistructure/PsiDepthFile.kt | 8 ++ .../codegpt/psistructure/PsiFileDepthQueue.kt | 37 ++++++ .../codegpt/psistructure/PsiFileQueue.kt | 19 --- .../psistructure/PsiStructureProvider.kt | 17 ++- .../ChatCompletionConfigurationForm.kt | 16 +++ .../CodeCompletionConfigurationForm.kt | 15 +++ .../configuration/ConfigurationSettings.kt | 2 + .../codegpt/ui/textarea/PromptTextField.kt | 4 + .../textarea/PromptTextFieldLookupManager.kt | 3 + .../codegpt/ui/textarea/SearchManager.kt | 2 + .../ui/textarea/TagDetailsComparator.kt | 4 +- .../ui/textarea/TagProcessorFactory.kt | 1 + .../textarea/header/UserInputHeaderPanel.kt | 22 +++- .../ui/textarea/header/tag/TagDetails.kt | 6 +- .../lookup/action/CodeAnalyzeActionItem.kt | 27 +++++ .../resources/messages/codegpt.properties | 5 + 21 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinPropertyAnalyzer.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiDepthFile.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileDepthQueue.kt delete mode 100644 src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileQueue.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/CodeAnalyzeActionItem.kt diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/structure/data/PsiStructureRepository.kt b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/structure/data/PsiStructureRepository.kt index c4803321..e9604d98 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/structure/data/PsiStructureRepository.kt +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/structure/data/PsiStructureRepository.kt @@ -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().state + .chatCompletionSettings + .psiStructureEnabled = false + disable() + } else { + service().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() + .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()} } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index dc35dcdb..a03440c5 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -63,6 +63,8 @@ public class TotalTokensPanel extends JPanel { if (ConfigurationSettings.getState().getChatCompletionSettings() .getPsiStructureEnabled()) { updatePsiTokenCount(psiTokens); + } else { + updatePsiTokenCount(0); } return Unit.INSTANCE; } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt index 9639a1e1..ba10040f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt @@ -40,7 +40,8 @@ object InfillRequestUtil { } if (service().state.codeCompletionSettings.collectDependencyStructure) { - val psiStructure = PsiStructureProvider().get(listOf(request.file)) + val depth = service().state.codeCompletionSettings.psiStructureAnalyzeDepth + val psiStructure = PsiStructureProvider().get(listOf(request.file), depth) if (psiStructure.isNotEmpty()) { infillRequestBuilder.addDependenciesStructure(psiStructure) infillRequestBuilder.addRepositoryName(psiStructure.first().repositoryName) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinFileAnalyzer.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinFileAnalyzer.kt index 00f90452..6854960f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinFileAnalyzer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinFileAnalyzer.kt @@ -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 by lazy { val types = mutableListOf() @@ -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) } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinPropertyAnalyzer.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinPropertyAnalyzer.kt new file mode 100644 index 00000000..9370f4a5 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/KotlinPropertyAnalyzer.kt @@ -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" \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiDepthFile.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiDepthFile.kt new file mode 100644 index 00000000..495cacc4 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiDepthFile.kt @@ -0,0 +1,8 @@ +package ee.carlrobert.codegpt.psistructure + +import com.intellij.psi.PsiFile + +data class PsiDepthFile( + val psiFile: PsiFile, + val depth: Int, +) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileDepthQueue.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileDepthQueue.kt new file mode 100644 index 00000000..cc4af7ac --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileDepthQueue.kt @@ -0,0 +1,37 @@ +package ee.carlrobert.codegpt.psistructure + +import com.intellij.psi.PsiFile + +class PsiFileDepthQueue( + initial: List, + private val maxDepth: Int = -1, +) { + + private val psiFiles = mutableSetOf().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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileQueue.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileQueue.kt deleted file mode 100644 index 5609f545..00000000 --- a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiFileQueue.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ee.carlrobert.codegpt.psistructure - -import com.intellij.psi.PsiFile - -class PsiFileQueue( - initial: List -) { - - private val queue = ArrayDeque(initial) - - @Synchronized - fun pop(): PsiFile? = - queue.removeFirstOrNull() - - @Synchronized - fun put(psiFile: PsiFile) { - queue.add(psiFile) - } -} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt index aa13e7cb..8c644cc4 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/psistructure/PsiStructureProvider.kt @@ -15,7 +15,10 @@ import kotlin.coroutines.cancellation.CancellationException class PsiStructureProvider { - suspend fun get(psiFiles: List): Set { + suspend fun get( + psiFiles: List, + analyzeDepth: Int, + ): Set { var result: Set? = null var attempts = 0 val maxAttempts = 5 @@ -35,17 +38,23 @@ class PsiStructureProvider { val future = ReadAction.nonBlocking> { val classStructureSet = mutableSetOf() val processedPsiFiles = mutableSetOf() - 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) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ChatCompletionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ChatCompletionConfigurationForm.kt index fe83a702..bdea6828 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ChatCompletionConfigurationForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ChatCompletionConfigurationForm.kt @@ -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().state.chatCompletionSettings.psiStructureEnabled ) + private val psiStructureAnalyzeDepthField = PortField().apply { + number = service().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 } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CodeCompletionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CodeCompletionConfigurationForm.kt index 98f950a2..e033e331 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CodeCompletionConfigurationForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CodeCompletionConfigurationForm.kt @@ -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().state.codeCompletionSettings.collectDependencyStructure ) + private val psiStructureAnalyzeDepthField = PortField().apply { + number = service().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 } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt index d3d22502..c2ed34ab 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt @@ -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) } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index 986354bf..37d0e693 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -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 = "" ) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldLookupManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldLookupManager.kt index d1b47c7b..0519d953 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldLookupManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldLookupManager.kt @@ -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, 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) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt index 659d4ad0..47117677 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt @@ -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) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagDetailsComparator.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagDetailsComparator.kt index 140b4fa3..468ffe5a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagDetailsComparator.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagDetailsComparator.kt @@ -8,12 +8,14 @@ internal class TagDetailsComparator : Comparator { } 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, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt index 3ccde477..831eccce 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt @@ -32,6 +32,7 @@ object TagProcessorFactory { is EditorTagDetails -> EditorTagProcessor(tagDetails) is ImageTagDetails -> ImageTagProcessor(tagDetails) is EmptyTagDetails -> TagProcessor { _, _ -> } + is CodeAnalyzeTagDetails -> TagProcessor { _, _ -> } } } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt index 77d51cb1..6f8e6064 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt @@ -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() + val filesVirtualFilesSet = allTags + .filterIsInstance() .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().state + .chatCompletionSettings + .psiStructureEnabled + + tagManager.addTag( + CodeAnalyzeTagDetails().apply { selected = psiStructureEnabled } + ) + EditorUtil.getOpenLocalFiles(project) .filterNot { it == selectedFile } .take(INITIAL_VISIBLE_FILES) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagDetails.kt index e335ff3a..76fce0c5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagDetails.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/tag/TagDetails.kt @@ -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("") \ No newline at end of file +class EmptyTagDetails : TagDetails("") + +class CodeAnalyzeTagDetails : TagDetails("Code Analyze", AllIcons.Actions.DependencyAnalyzer) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/CodeAnalyzeActionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/CodeAnalyzeActionItem.kt new file mode 100644 index 00000000..fdb71747 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/CodeAnalyzeActionItem.kt @@ -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()) + } +} \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 7e08ac36..9fda9acb 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -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