From 24b43e5158602766b688fd60d3ff268e602c1ee3 Mon Sep 17 00:00:00 2001 From: Lain Date: Tue, 22 Sep 2026 21:11:59 +0200 Subject: [PATCH 1/4] fix(navigate): keep find_symbols alive when a contributor refuses the platform call --- .../mcp/tools/code/NavigateTools.kt | 37 +++------- .../controller/mcp/tools/code/SymbolSearch.kt | 69 ++++++++++++++++++ .../mcp/tools/code/SymbolSearchTest.kt | 73 +++++++++++++++++++ 3 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt create mode 100644 src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt index 9bfdad28..4db90a50 100644 --- a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt @@ -42,37 +42,17 @@ internal class NavigateTools(private val project: Project) { val query = args.string("query") val max = args.int("max", DEFAULT_MAX) val libraries = args.boolean("libraries", false) - val rows = indexed { + val (outcome, rows) = indexed { val scope = if (libraries) GlobalSearchScope.allScope(project) else GlobalSearchScope.projectScope(project) val parameters = FindSymbolParameters.simple(project, libraries) - val items = LinkedHashSet() - for (contributor in contributors()) { - val names = LinkedHashSet() - contributor.processNames( - { name -> - if (name.contains(query, ignoreCase = true)) names += name - names.size < max - }, - scope, - null, - ) - for (name in names) { - if (items.size >= max) break - contributor.processElementsWithName( - name, - { item -> - items += item - items.size < max - }, - parameters, - ) - } - if (items.size >= max) break - } - items.filter { it is PsiElement && Locations.located(it) }.map(::symbolRow) + val sources = contributors().map { ContributorSymbolSource(it, scope, parameters) } + val outcome = SymbolSearch.collect(sources, query, max) + if (outcome.refusedEverywhere) throw ToolException(NO_SYMBOL_INDEX) + outcome to outcome.items.filter { it is PsiElement && Locations.located(it) }.map(::symbolRow) } return buildJsonObject { put("query", query) + if (outcome.partial) put("partial", true) table("symbols", rows, rows.size >= max) } } @@ -144,6 +124,11 @@ internal class NavigateTools(private val project: Project) { private const val DEFAULT_MAX = 50 + private const val NO_SYMBOL_INDEX = + "this IDE keeps its symbols outside the platform's name index, so no contributor answered: Rider " + + "serves them from the ReSharper backend, over its own protocol. Use search_text, find_files or " + + "file_outline instead; definition, references and implementations work as usual." + val FIND_SYMBOLS = ToolSpec( "find_symbols", "Finds classes, functions and other named symbols whose name contains the query, as the IDE's Go to Symbol does; " + diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt new file mode 100644 index 00000000..f3c7e283 --- /dev/null +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt @@ -0,0 +1,69 @@ +package dev.lain.claudejb.controller.mcp.tools.code + +import com.intellij.navigation.ChooseByNameContributorEx +import com.intellij.navigation.NavigationItem +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.indexing.FindSymbolParameters + +internal fun interface SymbolSource { + + fun symbolsInto(query: String, max: Int, into: MutableSet) +} + +internal class ContributorSymbolSource( + private val contributor: ChooseByNameContributorEx, + private val scope: GlobalSearchScope, + private val parameters: FindSymbolParameters, +) : SymbolSource { + + override fun symbolsInto(query: String, max: Int, into: MutableSet) { + for (name in namesMatching(query, max)) { + if (into.size >= max) break + contributor.processElementsWithName(name, { item -> gather(item, into, max) }, parameters) + } + } + + private fun namesMatching(query: String, max: Int): Set { + val names = LinkedHashSet() + contributor.processNames({ name -> keep(name, query, names, max) }, scope, null) + return names + } + + private fun keep(name: String, query: String, names: MutableSet, max: Int): Boolean { + if (name.contains(query, ignoreCase = true)) names += name + return names.size < max + } + + private fun gather(item: NavigationItem, into: MutableSet, max: Int): Boolean { + into += item + return into.size < max + } +} + +internal object SymbolSearch { + + data class Outcome(val items: List, val sources: Int, val refused: Int) { + + val refusedEverywhere: Boolean = sources > 0 && refused == sources + + val partial: Boolean = refused in 1 until sources + } + + fun collect(sources: List, query: String, max: Int): Outcome { + val items = LinkedHashSet() + var refused = 0 + for (source in sources) { + if (items.size >= max) break + if (!answered(source, query, max, items)) refused++ + } + return Outcome(items.toList(), sources.size, refused) + } + + private fun answered(source: SymbolSource, query: String, max: Int, into: MutableSet): Boolean = + try { + source.symbolsInto(query, max, into) + true + } catch (_: UnsupportedOperationException) { + false + } +} diff --git a/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt new file mode 100644 index 00000000..b6e48b01 --- /dev/null +++ b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt @@ -0,0 +1,73 @@ +package dev.lain.claudejb.controller.mcp.tools.code + +import com.intellij.navigation.ItemPresentation +import com.intellij.navigation.NavigationItem +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SymbolSearchTest { + + @Test + fun `a source that refuses the platform call does not take the ones that answer down with it`() { + val outcome = SymbolSearch.collect(listOf(refusing(), answering("Alpha")), "a", 10) + + assertEquals(listOf("Alpha"), outcome.items.map { it.name }) + assertEquals(1, outcome.refused) + assertFalse(outcome.refusedEverywhere) + assertTrue(outcome.partial) { "a list missing a whole contributor must not read as the complete answer" } + } + + @Test + fun `when every source refuses, the outcome says so instead of reading as a project without symbols`() { + val outcome = SymbolSearch.collect(listOf(refusing(), refusing()), "a", 10) + + assertTrue(outcome.items.isEmpty()) + assertTrue(outcome.refusedEverywhere) { "Rider refuses every contributor; an empty list there would be a lie" } + assertFalse(outcome.partial) + } + + @Test + fun `a refusal is not swallowed as an empty contributor, so the count stays honest`() { + val outcome = SymbolSearch.collect(listOf(answering(), refusing()), "a", 10) + + assertEquals(1, outcome.refused) + assertEquals(2, outcome.sources) + } + + @Test + fun `no further source is asked once max is reached`() { + val asked = mutableListOf() + val counting = SymbolSource { query, _, into -> + asked += query + into += item("symbol${asked.size}") + } + + val outcome = SymbolSearch.collect(listOf(counting, counting), "symbol", 1) + + assertEquals(listOf("symbol"), asked) + assertEquals(1, outcome.items.size) + } + + private fun refusing(): SymbolSource = SymbolSource { _, _, _ -> + throw UnsupportedOperationException("Use RdChooseByNameContributor.processNamesLifetimed") + } + + private fun answering(vararg names: String): SymbolSource = SymbolSource { _, _, into -> + names.forEach { into += item(it) } + } + + private fun item(named: String): NavigationItem = object : NavigationItem { + + override fun getName(): String = named + + override fun getPresentation(): ItemPresentation? = null + + override fun navigate(requestFocus: Boolean) = Unit + + override fun canNavigate(): Boolean = false + + override fun canNavigateToSource(): Boolean = false + } +} From cae15aae7b726f5c52b65ae1c8ca66693fec4aa9 Mon Sep 17 00:00:00 2001 From: Lain Date: Tue, 22 Sep 2026 21:12:01 +0200 Subject: [PATCH 2/4] build: bump the version to 6.0.1 --- CHANGELOG.md | 11 +++++++++++ RELEASE_NOTES.md | 11 +++++++++++ build.gradle.kts | 2 +- .../kotlin/dev/lain/claudejb/util/PluginIdentity.kt | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d1107d9..c27976dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.0.1] — 2026-09-22 + +### Fixed +- **`find_symbols` no longer fails outright in Rider.** A contributor that refuses the platform's + `processNames` call — which Rider's do, because its symbols live in the ReSharper backend and are + served only over its own protocol — is skipped instead of taking the whole tool down with + `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`. When every + contributor refuses, the tool says so and names what does work there (`search_text`, `find_files`, + `file_outline`, `definition`, `references`, `implementations`) rather than answering as though the + project had no symbols; when only some refuse, the answer carries `partial`. + ## [6.0.0] — 2026-09-12 **Claude becomes one with your IDE.** The plugin now runs four MCP servers of its own inside the IDE diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 063c4357..69b8ea94 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,14 @@ +## v6.0.1 — 2026-09-22 + +**`find_symbols` works in Rider again.** Asking Claude for a symbol there failed outright with +`UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`: Rider keeps its +symbols in the ReSharper backend and its contributors refuse the platform call the tool was making. A +contributor that refuses is now skipped rather than taking the tool down with it, and when every one of +them refuses Claude is told which tools do work there instead of being handed an empty list it would +read as "this project has no symbols". A partial answer is marked as partial. + +Nothing else changed. + ## v6.0.0 — 2026-09-12 **Claude becomes one with your IDE.** This release gives Claude the IDE itself as its instrument. diff --git a/build.gradle.kts b/build.gradle.kts index 916c56c9..24573fd0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ plugins { } group = "dev.lain" -version = "6.0.0" +version = "6.0.1" repositories { mavenCentral() diff --git a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt index d0dc7b1f..db63029d 100644 --- a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt +++ b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt @@ -2,7 +2,7 @@ package dev.lain.claudejb.util object PluginIdentity { - const val PLUGIN_VERSION = "6.0.0" + const val PLUGIN_VERSION = "6.0.1" const val NOTIFICATION_GROUP = "Claude Code" From 2a23b06c1d141015480f782f2ace448ecde6099f Mon Sep 17 00:00:00 2001 From: Lain Date: Tue, 22 Sep 2026 21:24:39 +0200 Subject: [PATCH 3/4] fix(navigate): say that nothing answered instead of returning an empty symbol list --- CHANGELOG.md | 9 +++++---- RELEASE_NOTES.md | 7 ++++--- .../controller/mcp/tools/code/NavigateTools.kt | 13 +++++++------ .../controller/mcp/tools/code/SymbolSearch.kt | 4 ++-- .../mcp/tools/code/SymbolSearchTest.kt | 16 ++++++++++++++-- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c27976dc..125fa35e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **`find_symbols` no longer fails outright in Rider.** A contributor that refuses the platform's `processNames` call — which Rider's do, because its symbols live in the ReSharper backend and are served only over its own protocol — is skipped instead of taking the whole tool down with - `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`. When every - contributor refuses, the tool says so and names what does work there (`search_text`, `find_files`, - `file_outline`, `definition`, `references`, `implementations`) rather than answering as though the - project had no symbols; when only some refuse, the answer carries `partial`. + `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`. When the + refusals leave nothing to return, the tool says how many contributors refused and names what does + work there (`search_text`, `find_files`, `file_outline`, `definition`, `references`, + `implementations`), rather than returning an empty list that reads as an empty project or a stale + index; an answer that did come back, shortened by a refusal, carries `partial`. ## [6.0.0] — 2026-09-12 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 69b8ea94..e927c307 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,9 +3,10 @@ **`find_symbols` works in Rider again.** Asking Claude for a symbol there failed outright with `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`: Rider keeps its symbols in the ReSharper backend and its contributors refuse the platform call the tool was making. A -contributor that refuses is now skipped rather than taking the tool down with it, and when every one of -them refuses Claude is told which tools do work there instead of being handed an empty list it would -read as "this project has no symbols". A partial answer is marked as partial. +contributor that refuses is now skipped rather than taking the tool down with it, and when the refusals +leave nothing to return Claude is told so, with the count and with the tools that do work there, instead +of being handed an empty list it would read as "this project has no symbols" or "the index is still +warming up". An answer that did come back, shortened by a refusal, is marked as partial. Nothing else changed. diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt index 4db90a50..0f6c30d2 100644 --- a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt @@ -47,7 +47,7 @@ internal class NavigateTools(private val project: Project) { val parameters = FindSymbolParameters.simple(project, libraries) val sources = contributors().map { ContributorSymbolSource(it, scope, parameters) } val outcome = SymbolSearch.collect(sources, query, max) - if (outcome.refusedEverywhere) throw ToolException(NO_SYMBOL_INDEX) + if (outcome.nothingAnswered) throw ToolException(noSymbolIndex(outcome)) outcome to outcome.items.filter { it is PsiElement && Locations.located(it) }.map(::symbolRow) } return buildJsonObject { @@ -94,6 +94,12 @@ internal class NavigateTools(private val project: Project) { return buildJsonObject { table("implementations", rows, rows.size >= max) } } + private fun noSymbolIndex(outcome: SymbolSearch.Outcome): String = + "nothing answered and ${outcome.refused} of ${outcome.sources} symbol contributors refused the call, so " + + "this is not an empty project and not a stale index: this IDE serves its symbols from a backend the " + + "platform's name index does not reach, as Rider does through ReSharper. Use search_text, find_files " + + "or file_outline instead; definition, references and implementations work as usual." + private fun resolved(args: ToolArgs): PsiElement = Locations.declarationAt(project, args) private fun symbolRow(item: NavigationItem): JsonObject = buildJsonObject { @@ -124,11 +130,6 @@ internal class NavigateTools(private val project: Project) { private const val DEFAULT_MAX = 50 - private const val NO_SYMBOL_INDEX = - "this IDE keeps its symbols outside the platform's name index, so no contributor answered: Rider " + - "serves them from the ReSharper backend, over its own protocol. Use search_text, find_files or " + - "file_outline instead; definition, references and implementations work as usual." - val FIND_SYMBOLS = ToolSpec( "find_symbols", "Finds classes, functions and other named symbols whose name contains the query, as the IDE's Go to Symbol does; " + diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt index f3c7e283..9dd57287 100644 --- a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt @@ -44,9 +44,9 @@ internal object SymbolSearch { data class Outcome(val items: List, val sources: Int, val refused: Int) { - val refusedEverywhere: Boolean = sources > 0 && refused == sources + val nothingAnswered: Boolean = refused > 0 && items.isEmpty() - val partial: Boolean = refused in 1 until sources + val partial: Boolean = refused > 0 && items.isNotEmpty() } fun collect(sources: List, query: String, max: Int): Outcome { diff --git a/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt index b6e48b01..fcbc43e7 100644 --- a/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt +++ b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt @@ -15,7 +15,7 @@ class SymbolSearchTest { assertEquals(listOf("Alpha"), outcome.items.map { it.name }) assertEquals(1, outcome.refused) - assertFalse(outcome.refusedEverywhere) + assertFalse(outcome.nothingAnswered) assertTrue(outcome.partial) { "a list missing a whole contributor must not read as the complete answer" } } @@ -24,7 +24,19 @@ class SymbolSearchTest { val outcome = SymbolSearch.collect(listOf(refusing(), refusing()), "a", 10) assertTrue(outcome.items.isEmpty()) - assertTrue(outcome.refusedEverywhere) { "Rider refuses every contributor; an empty list there would be a lie" } + assertTrue(outcome.nothingAnswered) { "Rider refuses every contributor; an empty list there would be a lie" } + assertFalse(outcome.partial) { "partial belongs to a shortened answer, not to no answer at all" } + } + + @Test + fun `a refusal next to a source with nothing to say is still no answer, not an empty project`() { + val outcome = SymbolSearch.collect(listOf(answering(), refusing()), "a", 10) + + assertTrue(outcome.items.isEmpty()) + assertTrue(outcome.nothingAnswered) { + "this is the Rider case: something answered with nothing while the contributors that hold the " + + "symbols refused, and an empty list there reads as a stale index" + } assertFalse(outcome.partial) } From 23e96aaa515e4f310a77b0b6759f5068ec605f95 Mon Sep 17 00:00:00 2001 From: Lain Date: Tue, 22 Sep 2026 21:39:57 +0200 Subject: [PATCH 4/4] fix(navigate): report the refusals beside the result instead of failing a fruitless search --- CHANGELOG.md | 10 ++++----- RELEASE_NOTES.md | 8 +++---- .../mcp/tools/code/NavigateTools.kt | 22 ++++++++++++++----- .../controller/mcp/tools/code/SymbolSearch.kt | 4 ++-- .../mcp/tools/code/SymbolSearchTest.kt | 19 ++++++++-------- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 125fa35e..40fad6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **`find_symbols` no longer fails outright in Rider.** A contributor that refuses the platform's `processNames` call — which Rider's do, because its symbols live in the ReSharper backend and are served only over its own protocol — is skipped instead of taking the whole tool down with - `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`. When the - refusals leave nothing to return, the tool says how many contributors refused and names what does - work there (`search_text`, `find_files`, `file_outline`, `definition`, `references`, - `implementations`), rather than returning an empty list that reads as an empty project or a stale - index; an answer that did come back, shortened by a refusal, carries `partial`. + `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`. An answer + reached by asking fewer contributors than the IDE has carries `refused` and `contributors`, and when + it is also empty it carries a `note`, so an empty list is not read as an absent symbol or a stale + index. Only when every contributor refuses does the tool fail, naming what does work there + (`search_text`, `find_files`, `file_outline`, `definition`, `references`, `implementations`). ## [6.0.0] — 2026-09-12 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e927c307..dea38d0c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,10 +3,10 @@ **`find_symbols` works in Rider again.** Asking Claude for a symbol there failed outright with `UnsupportedOperationException: Use RdChooseByNameContributor.processNamesLifetimed`: Rider keeps its symbols in the ReSharper backend and its contributors refuse the platform call the tool was making. A -contributor that refuses is now skipped rather than taking the tool down with it, and when the refusals -leave nothing to return Claude is told so, with the count and with the tools that do work there, instead -of being handed an empty list it would read as "this project has no symbols" or "the index is still -warming up". An answer that did come back, shortened by a refusal, is marked as partial. +contributor that refuses is now skipped rather than taking the tool down with it, and the answer says how +many refused out of how many the IDE has — with a note when the result is also empty — so Claude does not +read an empty list as "this symbol does not exist" or "the index is still warming up". The tool only fails +when every contributor refuses, and then it names the tools that do work there. Nothing else changed. diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt index 0f6c30d2..ca3d4ec7 100644 --- a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/NavigateTools.kt @@ -47,12 +47,16 @@ internal class NavigateTools(private val project: Project) { val parameters = FindSymbolParameters.simple(project, libraries) val sources = contributors().map { ContributorSymbolSource(it, scope, parameters) } val outcome = SymbolSearch.collect(sources, query, max) - if (outcome.nothingAnswered) throw ToolException(noSymbolIndex(outcome)) + if (outcome.noneAnswered) throw ToolException(noSymbolIndex(outcome)) outcome to outcome.items.filter { it is PsiElement && Locations.located(it) }.map(::symbolRow) } return buildJsonObject { put("query", query) - if (outcome.partial) put("partial", true) + if (outcome.incomplete) { + put("refused", outcome.refused) + put("contributors", outcome.sources) + if (rows.isEmpty()) put("note", refusedNote(outcome)) + } table("symbols", rows, rows.size >= max) } } @@ -95,10 +99,16 @@ internal class NavigateTools(private val project: Project) { } private fun noSymbolIndex(outcome: SymbolSearch.Outcome): String = - "nothing answered and ${outcome.refused} of ${outcome.sources} symbol contributors refused the call, so " + - "this is not an empty project and not a stale index: this IDE serves its symbols from a backend the " + - "platform's name index does not reach, as Rider does through ReSharper. Use search_text, find_files " + - "or file_outline instead; definition, references and implementations work as usual." + "every one of the ${outcome.sources} symbol contributors refused the call: this IDE serves its symbols " + + "from a backend the platform's name index does not reach, as Rider does through ReSharper. Use " + + "search_text, find_files or file_outline instead; definition, references and implementations work " + + "as usual." + + private fun refusedNote(outcome: SymbolSearch.Outcome): String = + "${outcome.refused} of the ${outcome.sources} symbol contributors refused the call, so what they hold was " + + "never searched: those symbols live in a backend the platform's name index does not reach, as Rider's " + + "do through ReSharper. An empty result here does not mean the symbol is absent, and does not mean the " + + "index is stale; search_text and file_outline see what they hold." private fun resolved(args: ToolArgs): PsiElement = Locations.declarationAt(project, args) diff --git a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt index 9dd57287..fe84267d 100644 --- a/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt +++ b/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearch.kt @@ -44,9 +44,9 @@ internal object SymbolSearch { data class Outcome(val items: List, val sources: Int, val refused: Int) { - val nothingAnswered: Boolean = refused > 0 && items.isEmpty() + val noneAnswered: Boolean = sources > 0 && refused == sources - val partial: Boolean = refused > 0 && items.isNotEmpty() + val incomplete: Boolean = refused > 0 } fun collect(sources: List, query: String, max: Int): Outcome { diff --git a/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt index fcbc43e7..99c1cd4a 100644 --- a/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt +++ b/src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SymbolSearchTest.kt @@ -15,8 +15,8 @@ class SymbolSearchTest { assertEquals(listOf("Alpha"), outcome.items.map { it.name }) assertEquals(1, outcome.refused) - assertFalse(outcome.nothingAnswered) - assertTrue(outcome.partial) { "a list missing a whole contributor must not read as the complete answer" } + assertFalse(outcome.noneAnswered) + assertTrue(outcome.incomplete) { "a list missing a whole contributor must not read as the complete answer" } } @Test @@ -24,20 +24,19 @@ class SymbolSearchTest { val outcome = SymbolSearch.collect(listOf(refusing(), refusing()), "a", 10) assertTrue(outcome.items.isEmpty()) - assertTrue(outcome.nothingAnswered) { "Rider refuses every contributor; an empty list there would be a lie" } - assertFalse(outcome.partial) { "partial belongs to a shortened answer, not to no answer at all" } + assertTrue(outcome.noneAnswered) { "with nothing left to ask, an empty list would be a lie" } } @Test - fun `a refusal next to a source with nothing to say is still no answer, not an empty project`() { - val outcome = SymbolSearch.collect(listOf(answering(), refusing()), "a", 10) + fun `sources that answer with nothing are an empty result, not a failure`() { + val outcome = SymbolSearch.collect(listOf(answering(), answering(), refusing()), "a", 10) assertTrue(outcome.items.isEmpty()) - assertTrue(outcome.nothingAnswered) { - "this is the Rider case: something answered with nothing while the contributors that hold the " + - "symbols refused, and an empty list there reads as a stale index" + assertFalse(outcome.noneAnswered) { + "most contributors answered and simply held no match; failing here would turn every fruitless " + + "search in that IDE into an error" } - assertFalse(outcome.partial) + assertTrue(outcome.incomplete) { "the refusal still has to be reported alongside the empty list" } } @Test