From 80ba956c5e39f47f693787f5e84098cd7af046cb Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 16 Sep 2025 01:34:34 +0100 Subject: [PATCH] feat: add Diagnostics tag --- .../structure/data/PsiStructureRepository.kt | 3 + .../ui/textarea/PromptTextFieldConstants.kt | 1 + .../codegpt/ui/textarea/SearchManager.kt | 2 + .../ui/textarea/TagProcessorFactory.kt | 121 ++++++++++++++++++ .../ui/textarea/header/tag/TagDetails.kt | 5 +- .../lookup/action/DiagnosticsActionItem.kt | 41 ++++++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/DiagnosticsActionItem.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 b8157053..97919fdb 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 @@ -226,6 +226,7 @@ class PsiStructureRepository( is WebTagDetails -> null is ImageTagDetails -> null is CodeAnalyzeTagDetails -> null + is DiagnosticsTagDetails -> null } virtualFile?.takeIf { it.isValid && it.exists()} @@ -253,6 +254,7 @@ class PsiStructureRepository( is WebTagDetails -> false is ImageTagDetails -> false is CodeAnalyzeTagDetails -> false + is DiagnosticsTagDetails -> false } } .toSet() @@ -280,6 +282,7 @@ class PsiStructureRepository( is WebTagDetails -> null is ImageTagDetails -> null is CodeAnalyzeTagDetails -> null + is DiagnosticsTagDetails -> null } virtualFile?.takeIf { it.isValid && it.exists()} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldConstants.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldConstants.kt index 180a481e..049c36c3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldConstants.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldConstants.kt @@ -17,6 +17,7 @@ object PromptTextFieldConstants { "history", "hist", "h", "personas", "persona", "p", "docs", "doc", "d", + "diagnostics", "diagnostic", "diag", "mcp", "m", "web", "w", "image", "img", "i" 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 47117677..92c63d89 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt @@ -10,6 +10,7 @@ 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.action.DiagnosticsActionItem import ee.carlrobert.codegpt.ui.textarea.lookup.group.* import kotlinx.coroutines.CancellationException @@ -35,6 +36,7 @@ class SearchManager( PersonasGroupItem(tagManager), DocsGroupItem(tagManager), CodeAnalyzeActionItem(tagManager), + DiagnosticsActionItem(tagManager), MCPGroupItem(), WebActionItem(tagManager), ImageActionItem(project, tagManager) 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 831eccce..b7fb86fa 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt @@ -1,9 +1,17 @@ package ee.carlrobert.codegpt.ui.textarea +import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl +import com.intellij.codeInsight.daemon.impl.HighlightInfo +import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.completions.CompletionRequestUtil import ee.carlrobert.codegpt.conversations.Conversation @@ -33,6 +41,7 @@ object TagProcessorFactory { is ImageTagDetails -> ImageTagProcessor(tagDetails) is EmptyTagDetails -> TagProcessor { _, _ -> } is CodeAnalyzeTagDetails -> TagProcessor { _, _ -> } + is DiagnosticsTagDetails -> DiagnosticsTagProcessor(project, tagDetails) } } } @@ -248,4 +257,116 @@ class ConversationTagProcessor( } message.conversationsHistoryIds?.add(tagDetails.conversationId) } +} + +class DiagnosticsTagProcessor( + private val project: Project, + private val tagDetails: DiagnosticsTagDetails, +) : TagProcessor { + override fun process(message: Message, promptBuilder: StringBuilder) { + promptBuilder + .append("\n## Current File Problems\n") + .append(getDiagnosticsString(project, tagDetails.virtualFile)) + .append("\n") + } + + private fun getDiagnosticsString(project: Project, virtualFile: VirtualFile): String { + return try { + DumbService.getInstance(project).runReadActionInSmartMode { + val document = FileDocumentManager.getInstance().getDocument(virtualFile) + ?: return@runReadActionInSmartMode "No document found for file" + + PsiDocumentManager.getInstance(project).commitDocument(document) + + val psiManager = PsiManager.getInstance(project) + val psiFile = psiManager.findFile(virtualFile) + ?: return@runReadActionInSmartMode "No PSI file found for: ${virtualFile.path}" + + val rangeHighlights = + DaemonCodeAnalyzerImpl.getHighlights( + document, + HighlightSeverity.WEAK_WARNING, + project + ) + // TODO: Find a better solution + val fileLevel: List = try { + val method = DaemonCodeAnalyzerImpl::class.java.methods.firstOrNull { + it.name == "getFileLevelHighlights" && it.parameterCount == 2 + } + if (method != null) { + @Suppress("UNCHECKED_CAST") + method.invoke(null, project, psiFile) as? List ?: emptyList() + } else { + emptyList() + } + } catch (_: Throwable) { + emptyList() + } + + val highlights = (rangeHighlights.asSequence() + fileLevel.asSequence()) + .distinctBy { Triple(it.description, it.startOffset, it.severity) } + .sortedWith( + compareBy( + { severityOrder(it.severity) }, + { it.startOffset.coerceAtLeast(0) } + ) + ) + .toList() + + if (highlights.isEmpty()) { + return@runReadActionInSmartMode "" + } + + val maxItems = 200 + val overflow = (highlights.size - maxItems).coerceAtLeast(0) + val shown = highlights.take(maxItems) + + buildString { + append("File: ${virtualFile.name}\n") + append("Path: ${virtualFile.path}\n\n") + + shown.forEach { info -> + val startOffset = info.startOffset.coerceIn(0, document.textLength) + val lineColText = + if (info.startOffset >= 0 && document.textLength > 0) { + val line = document.getLineNumber(startOffset) + 1 + val col = startOffset - document.getLineStartOffset(line - 1) + 1 + "line $line, col $col" + } else { + "file-level" + } + + val rawMessage = info.description ?: info.toolTip ?: "" + val message = StringUtil.removeHtmlTags(rawMessage, false).trim() + + val severityLabel = when (info.severity) { + HighlightSeverity.ERROR -> "ERROR" + HighlightSeverity.WARNING -> "WARNING" + HighlightSeverity.WEAK_WARNING -> "WEAK_WARNING" + HighlightSeverity.INFORMATION -> "INFO" + else -> info.severity.toString() + } + + append("- [$severityLabel] $lineColText: $message\n") + } + + if (overflow > 0) { + append("... ($overflow more not shown)\n") + } + } + } + } catch (e: Exception) { + "Error retrieving diagnostics: ${e.message}" + } + } + + private fun severityOrder(severity: HighlightSeverity): Int { + return when (severity) { + HighlightSeverity.ERROR -> 0 + HighlightSeverity.WARNING -> 1 + HighlightSeverity.WEAK_WARNING -> 2 + HighlightSeverity.INFORMATION -> 3 + else -> 4 + } + } } \ No newline at end of file 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 76fce0c5..08ed3c94 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 @@ -131,4 +131,7 @@ data class HistoryTagDetails( class EmptyTagDetails : TagDetails("") -class CodeAnalyzeTagDetails : TagDetails("Code Analyze", AllIcons.Actions.DependencyAnalyzer) \ No newline at end of file +class CodeAnalyzeTagDetails : TagDetails("Code Analyze", AllIcons.Actions.DependencyAnalyzer) + +data class DiagnosticsTagDetails(val virtualFile: VirtualFile) : + TagDetails("${virtualFile.name} Problems", AllIcons.General.InspectionsEye) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/DiagnosticsActionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/DiagnosticsActionItem.kt new file mode 100644 index 00000000..08b473c7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/DiagnosticsActionItem.kt @@ -0,0 +1,41 @@ +package ee.carlrobert.codegpt.ui.textarea.lookup.action + +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel +import ee.carlrobert.codegpt.ui.textarea.header.tag.DiagnosticsTagDetails +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.TagManager +import ee.carlrobert.codegpt.util.EditorUtil + +class DiagnosticsActionItem( + private val tagManager: TagManager +) : AbstractLookupActionItem() { + + override val displayName: String = "Diagnostics" + override val icon = AllIcons.General.InspectionsEye + override val enabled: Boolean + get() = tagManager.getTags().none { it is DiagnosticsTagDetails } && + tagManager.getTags().any { it is FileTagDetails || it is EditorTagDetails } + + override fun execute(project: Project, userInputPanel: UserInputPanel) { + val virtualFile = findVirtualFile(project) + virtualFile?.let { file -> + userInputPanel.addTag(DiagnosticsTagDetails(file)) + } + } + + private fun findVirtualFile(project: Project): VirtualFile? { + val existingFile = tagManager.getTags() + .firstNotNullOfOrNull { tag -> + when (tag) { + is FileTagDetails -> tag.virtualFile + is EditorTagDetails -> tag.virtualFile + else -> null + } + } + return existingFile ?: EditorUtil.getSelectedEditor(project)?.virtualFile + } +} \ No newline at end of file