Skip to content

Commit b8fc22b

Browse files
authored
refactor: remove internal API usage (#152)
* chore: restore CHANGELOG for v1.0.0 release * refactor(CodeQualityMcpTools, ScopeSymbolSearchMcpTools): replace runReadAction with readAction * refactor(style): apply quick fix for style
1 parent 6273dae commit b8fc22b

5 files changed

Lines changed: 188 additions & 131 deletions

File tree

plugins/WorkspaceMcpTools/CHANGELOG.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@
66

77
### Added
88

9-
### Changed
10-
11-
### Deprecated
12-
13-
### Removed
14-
15-
### Fixed
16-
17-
### Security
18-
19-
## [1.0.0] - 2026-02-27
20-
21-
### Added
22-
239
- Initial plugin scaffold with MCP Server dependency and VFS tool placeholders.
2410
- `vfs_read_file` now supports `FULL`, `CHAR_RANGE`, and `LINE_RANGE` read strategies.
2511
- Added convenience tools: `vfs_read_file_full`, `vfs_read_file_by_char_range`, `vfs_read_file_by_line_range`.
@@ -79,5 +65,3 @@
7965
- `ScopeProgramOp` JSON decoding now accepts legacy token name `ATOM` as an alias of `PUSH_ATOM`, fixing
8066
backward compatibility for previously serialized scope programs.
8167

82-
[Unreleased]: https://github.com/ghostflyby/IntelliJ-Plugins/compare/WorkspaceMcpTools-v1.0.0...HEAD
83-
[1.0.0]: https://github.com/ghostflyby/IntelliJ-Plugins/commits/WorkspaceMcpTools-v1.0.0

plugins/WorkspaceMcpTools/src/main/kotlin/dev/ghostflyby/mcp/quality/CodeQualityMcpTools.kt

Lines changed: 147 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
@file:Suppress("UnstableApiUsage")
2-
31
/*
42
* Copyright (c) 2026 ghostflyby
53
* SPDX-FileCopyrightText: 2026 ghostflyby
@@ -27,14 +25,9 @@ package dev.ghostflyby.mcp.quality
2725
import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor
2826
import com.intellij.codeInsight.actions.OptimizeImportsProcessor
2927
import com.intellij.codeInsight.actions.ReformatCodeProcessor
30-
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
31-
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl
32-
import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator
33-
import com.intellij.codeInsight.daemon.impl.HighlightInfo
34-
import com.intellij.codeInsight.daemon.impl.HighlightingSessionImpl
35-
import com.intellij.codeInsight.multiverse.defaultContext
36-
import com.intellij.codeInspection.InspectionProfile
28+
import com.intellij.codeInspection.*
3729
import com.intellij.codeInspection.ex.GlobalInspectionContextBase
30+
import com.intellij.codeInspection.ex.InspectionToolWrapper
3831
import com.intellij.lang.annotation.HighlightSeverity
3932
import com.intellij.mcpserver.McpToolset
4033
import com.intellij.mcpserver.annotations.McpDescription
@@ -48,36 +41,19 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
4841
import com.intellij.openapi.progress.ProgressManager
4942
import com.intellij.openapi.roots.ContentIterator
5043
import com.intellij.openapi.roots.ProjectRootManager
51-
import com.intellij.openapi.util.ProperTextRange
5244
import com.intellij.openapi.util.TextRange
5345
import com.intellij.openapi.vfs.VirtualFile
5446
import com.intellij.platform.ide.progress.withBackgroundProgress
55-
import com.intellij.psi.PsiFile
56-
import com.intellij.psi.PsiManager
5747
import com.intellij.profile.codeInspection.InspectionProfileManager
5848
import com.intellij.profile.codeInspection.InspectionProjectProfileManager
49+
import com.intellij.psi.PsiErrorElement
50+
import com.intellij.psi.PsiFile
51+
import com.intellij.psi.PsiManager
52+
import com.intellij.psi.util.PsiTreeUtil
5953
import dev.ghostflyby.mcp.Bundle
60-
import dev.ghostflyby.mcp.common.ALLOW_UI_INTERACTIVE_SCOPES_PARAM_DESCRIPTION
61-
import dev.ghostflyby.mcp.common.AGENT_FIRST_CALL_SHORTCUT_DESCRIPTION_SUFFIX
62-
import dev.ghostflyby.mcp.common.VFS_URL_PARAM_DESCRIPTION
63-
import dev.ghostflyby.mcp.common.batchTry
64-
import dev.ghostflyby.mcp.common.findFileByUrlWithRefresh
65-
import dev.ghostflyby.mcp.common.relativizePathOrNull
66-
import dev.ghostflyby.mcp.common.reportActivity
67-
import dev.ghostflyby.mcp.scope.ScopeProgramDescriptorDto
68-
import dev.ghostflyby.mcp.scope.ScopeQuickPreset
69-
import dev.ghostflyby.mcp.scope.ScopeResolverService
70-
import dev.ghostflyby.mcp.scope.ScopeShape
71-
import dev.ghostflyby.mcp.scope.buildPresetScopeDescriptor
72-
import kotlinx.coroutines.CompletableDeferred
73-
import kotlinx.coroutines.Dispatchers
74-
import kotlinx.coroutines.coroutineScope
75-
import kotlinx.coroutines.currentCoroutineContext
76-
import kotlinx.coroutines.delay
77-
import kotlinx.coroutines.isActive
78-
import kotlinx.coroutines.launch
79-
import kotlinx.coroutines.withContext
80-
import kotlinx.coroutines.withTimeoutOrNull
54+
import dev.ghostflyby.mcp.common.*
55+
import dev.ghostflyby.mcp.scope.*
56+
import kotlinx.coroutines.*
8157
import kotlinx.serialization.EncodeDefault
8258
import kotlinx.serialization.ExperimentalSerializationApi
8359
import kotlinx.serialization.Serializable
@@ -1160,51 +1136,161 @@ internal class CodeQualityMcpTools : McpToolset {
11601136
file: VirtualFile,
11611137
errorsOnly: Boolean,
11621138
): List<QualityProblemDto> {
1163-
val minSeverity = if (errorsOnly) HighlightSeverity.ERROR else HighlightSeverity.WEAK_WARNING
1164-
return readAction {
1139+
val minSeverity = if (errorsOnly) HighlightSeverity.ERROR else HighlightSeverity.WARNING
1140+
val psiFile = readAction {
11651141
if (file.isDirectory) {
11661142
mcpFail("URL points to a directory, not a file: ${file.url}")
11671143
}
11681144
if (file.fileType.isBinary) {
11691145
mcpFail("File at URL '${file.url}' is binary and cannot be analyzed.")
11701146
}
1171-
val psiFile = PsiManager.getInstance(project).findFile(file)
1147+
PsiManager.getInstance(project).findFile(file)
11721148
?: mcpFail("No PSI file available for URL: ${file.url}")
1173-
val document = FileDocumentManager.getInstance().getDocument(file)
1149+
}
1150+
val document = readAction {
1151+
FileDocumentManager.getInstance().getDocument(file)
11741152
?: mcpFail("No text document available for URL: ${file.url}")
1175-
val indicator = DaemonProgressIndicator()
1176-
val range = ProperTextRange(0, document.textLength)
1177-
val analyzer = DaemonCodeAnalyzer.getInstance(project) as? DaemonCodeAnalyzerImpl
1178-
?: mcpFail("Daemon code analyzer implementation is unavailable for this project.")
1179-
val problems = mutableListOf<QualityProblemDto>()
1180-
HighlightingSessionImpl.runInsideHighlightingSession(psiFile, defaultContext(), null, range, false) { session ->
1181-
(session as HighlightingSessionImpl).setMinimumSeverity(minSeverity)
1182-
val highlights = analyzer.runMainPasses(psiFile, document, indicator)
1183-
highlights.forEach { info ->
1184-
if (info.severity.myVal >= minSeverity.myVal) {
1185-
problems += createQualityProblem(document, info)
1186-
}
1187-
}
1153+
}
1154+
val profile = resolveInspectionProfile(project, null)
1155+
val inspectionManager = InspectionManager.getInstance(project)
1156+
val tools = readAction { collectEnabledInspectionTools(profile, psiFile) }
1157+
val toolSeverityByShortName = readAction {
1158+
tools.associate { wrapper ->
1159+
wrapper.shortName to resolveToolSeverity(profile, psiFile, wrapper)
11881160
}
1189-
problems
11901161
}
1162+
1163+
val deduplicated = linkedMapOf<String, QualityProblemDto>()
1164+
1165+
tools.forEach { tool ->
1166+
ProgressManager.checkCanceled()
1167+
val descriptors = InspectionEngine.runInspectionOnFile(
1168+
psiFile,
1169+
tool,
1170+
inspectionManager.createNewGlobalContext(),
1171+
)
1172+
descriptors.forEach { descriptor ->
1173+
val severity = resolveProblemSeverity(
1174+
descriptor,
1175+
toolSeverityByShortName[tool.shortName] ?: HighlightSeverity.WARNING,
1176+
)
1177+
if (severity.myVal < minSeverity.myVal) return@forEach
1178+
val problem = buildProblemFromDescriptor(document, descriptor, severity)
1179+
?: return@forEach
1180+
deduplicated.putIfAbsent(
1181+
"${problem.severity}:${problem.line}:${problem.column}:${problem.description}",
1182+
problem,
1183+
)
1184+
}
1185+
}
1186+
1187+
val syntaxErrors = readAction { PsiTreeUtil.findChildrenOfType(psiFile, PsiErrorElement::class.java) }
1188+
syntaxErrors.forEach { error ->
1189+
val severity = HighlightSeverity.ERROR
1190+
if (severity.myVal < minSeverity.myVal) return@forEach
1191+
val problem = createQualityProblem(
1192+
document = document,
1193+
severity = severity.name,
1194+
description = error.errorDescription,
1195+
startOffset = error.textRange.startOffset,
1196+
)
1197+
deduplicated.putIfAbsent(
1198+
"${problem.severity}:${problem.line}:${problem.column}:${problem.description}",
1199+
problem,
1200+
)
1201+
}
1202+
1203+
return deduplicated.values.toList()
11911204
}
11921205

1193-
private fun createQualityProblem(document: Document, info: HighlightInfo): QualityProblemDto {
1194-
val startLine = document.getLineNumber(info.startOffset)
1206+
private fun createQualityProblem(
1207+
document: Document,
1208+
severity: String,
1209+
description: String?,
1210+
startOffset: Int,
1211+
): QualityProblemDto {
1212+
val boundedOffset = startOffset.coerceIn(0, document.textLength)
1213+
val startLine = document.getLineNumber(boundedOffset)
11951214
val lineStartOffset = document.getLineStartOffset(startLine)
11961215
val lineEndOffset = document.getLineEndOffset(startLine)
11971216
val lineContent = document.getText(TextRange(lineStartOffset, lineEndOffset))
1198-
val column = info.startOffset - lineStartOffset
1217+
val column = boundedOffset - lineStartOffset
11991218
return QualityProblemDto(
1200-
severity = info.severity.name,
1201-
description = info.description,
1219+
severity = severity,
1220+
description = description,
12021221
lineContent = lineContent,
12031222
line = startLine + 1,
12041223
column = column + 1,
12051224
)
12061225
}
12071226

1227+
private fun collectEnabledInspectionTools(
1228+
profile: InspectionProfile,
1229+
psiFile: PsiFile,
1230+
): List<InspectionToolWrapper<*, *>> {
1231+
return profile.getInspectionTools(psiFile)
1232+
.asSequence()
1233+
.filter { tool ->
1234+
ProgressManager.checkCanceled()
1235+
val key = tool.displayKey ?: return@filter false
1236+
profile.isToolEnabled(key, psiFile)
1237+
}
1238+
.toList()
1239+
}
1240+
1241+
private fun resolveToolSeverity(
1242+
profile: InspectionProfile,
1243+
psiFile: PsiFile,
1244+
tool: InspectionToolWrapper<*, *>,
1245+
): HighlightSeverity {
1246+
val level = tool.displayKey?.let { key ->
1247+
profile.getErrorLevel(key, psiFile)
1248+
} ?: tool.defaultLevel
1249+
return level.severity
1250+
}
1251+
1252+
private fun resolveProblemSeverity(
1253+
descriptor: ProblemDescriptor,
1254+
configuredToolSeverity: HighlightSeverity,
1255+
): HighlightSeverity {
1256+
return when (descriptor.highlightType) {
1257+
ProblemHighlightType.ERROR,
1258+
ProblemHighlightType.GENERIC_ERROR,
1259+
-> HighlightSeverity.ERROR
1260+
1261+
ProblemHighlightType.WARNING -> HighlightSeverity.WARNING
1262+
ProblemHighlightType.WEAK_WARNING -> HighlightSeverity.WEAK_WARNING
1263+
ProblemHighlightType.INFORMATION,
1264+
-> HighlightSeverity.INFORMATION
1265+
1266+
else -> configuredToolSeverity
1267+
}
1268+
}
1269+
1270+
private suspend fun buildProblemFromDescriptor(
1271+
document: Document,
1272+
descriptor: ProblemDescriptor,
1273+
severity: HighlightSeverity,
1274+
): QualityProblemDto? {
1275+
val snapshot = readAction {
1276+
val anchor = descriptor.startElement ?: descriptor.psiElement ?: return@readAction null
1277+
val anchorRange = anchor.textRange ?: return@readAction null
1278+
val rangeInElement = descriptor.textRangeInElement
1279+
val startOffset = anchorRange.startOffset + rangeInElement.startOffset
1280+
val description = ProblemDescriptorUtil.renderDescriptionMessage(descriptor, anchor)
1281+
DescriptorProblemSnapshot(
1282+
startOffset = startOffset,
1283+
description = description,
1284+
)
1285+
} ?: return null
1286+
return createQualityProblem(
1287+
document = document,
1288+
severity = severity.name,
1289+
description = snapshot.description,
1290+
startOffset = snapshot.startOffset,
1291+
)
1292+
}
1293+
12081294
private suspend fun collectProjectContentFilesInScope(
12091295
project: com.intellij.openapi.project.Project,
12101296
resolvedScope: com.intellij.psi.search.SearchScope,
@@ -1343,6 +1429,11 @@ internal class CodeQualityMcpTools : McpToolset {
13431429
return relativizePathOrNull(projectBasePath, filePath)
13441430
}
13451431

1432+
private data class DescriptorProblemSnapshot(
1433+
val startOffset: Int,
1434+
val description: String,
1435+
)
1436+
13461437
private data class ScopeFileCollection(
13471438
val files: List<VirtualFile>,
13481439
val scannedFileCount: Int,

plugins/WorkspaceMcpTools/src/main/kotlin/dev/ghostflyby/mcp/scope/ScopeResolverService.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import com.intellij.mcpserver.mcpFail
2626
import com.intellij.openapi.application.readAction
2727
import com.intellij.openapi.components.Service
2828
import com.intellij.openapi.components.service
29-
import com.intellij.openapi.project.Project
3029
import com.intellij.openapi.module.ModuleManager
30+
import com.intellij.openapi.project.Project
3131
import com.intellij.psi.search.GlobalSearchScope
3232
import com.intellij.psi.search.GlobalSearchScopesCore
3333
import com.intellij.psi.search.LocalSearchScope
@@ -468,14 +468,14 @@ internal class ScopeResolverService {
468468
right: SearchScope,
469469
tokenIndex: Int,
470470
): SearchScope {
471-
return when {
472-
left is GlobalSearchScope && right is GlobalSearchScope -> left.intersectWith(right)
473-
left is GlobalSearchScope && right is LocalSearchScope -> left.intersectWith(right)
474-
left is LocalSearchScope && right is GlobalSearchScope -> right.intersectWith(left)
475-
left is LocalSearchScope && right is LocalSearchScope -> left.intersectWith(right)
471+
return when (left) {
472+
is GlobalSearchScope if right is GlobalSearchScope -> left.intersectWith(right)
473+
is GlobalSearchScope if right is LocalSearchScope -> left.intersectWith(right)
474+
is LocalSearchScope if right is GlobalSearchScope -> right.intersectWith(left)
475+
is LocalSearchScope if right is LocalSearchScope -> left.intersectWith(right)
476476
else -> mcpFail(
477477
"Token[$tokenIndex] AND is unsupported for scope types " +
478-
"'${left.javaClass.name}' and '${right.javaClass.name}'.",
478+
"'${left.javaClass.name}' and '${right.javaClass.name}'.",
479479
)
480480
}
481481
}
@@ -485,14 +485,14 @@ internal class ScopeResolverService {
485485
right: SearchScope,
486486
tokenIndex: Int,
487487
): SearchScope {
488-
return when {
489-
left is GlobalSearchScope && right is GlobalSearchScope -> left.uniteWith(right)
490-
left is GlobalSearchScope && right is LocalSearchScope -> left.union(right)
491-
left is LocalSearchScope && right is GlobalSearchScope -> right.union(left)
492-
left is LocalSearchScope && right is LocalSearchScope -> left.union(right)
488+
return when (left) {
489+
is GlobalSearchScope if right is GlobalSearchScope -> left.uniteWith(right)
490+
is GlobalSearchScope if right is LocalSearchScope -> left.union(right)
491+
is LocalSearchScope if right is GlobalSearchScope -> right.union(left)
492+
is LocalSearchScope if right is LocalSearchScope -> left.union(right)
493493
else -> mcpFail(
494494
"Token[$tokenIndex] OR is unsupported for scope types " +
495-
"'${left.javaClass.name}' and '${right.javaClass.name}'.",
495+
"'${left.javaClass.name}' and '${right.javaClass.name}'.",
496496
)
497497
}
498498
}

0 commit comments

Comments
 (0)