Skip to content

feat(ADFA-4824): Find usages in the Kotlin K2 LSP - #1624

Merged
itsaky-adfa merged 20 commits into
stagefrom
worktree/ADFA-4824
Aug 11, 2026
Merged

feat(ADFA-4824): Find usages in the Kotlin K2 LSP#1624
itsaky-adfa merged 20 commits into
stagefrom
worktree/ADFA-4824

Conversation

@itsaky-adfa

@itsaky-adfa itsaky-adfa commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Jira: ADFA-4824

Fills in KotlinLanguageServer.findReferences, which until now answered empty. From a Kotlin declaration - or a reference to one - list every usage in the workspace across all three scopes: same file, another file in the same module, another module.

Everything downstream (ReferenceResult, IDEEditor.onFindReferencesResult, the search-results panel) already existed for the Java server and is reused.

Full write-up: docs/features/kotlin-find-usages.md.

How it works

  • Target at caret - TargetAtCaret maps the caret to the declaration to search for, either directly (caret on the declaration's own name) or by resolving the reference under it. Go-to-def's referenceAtCaret can't be reused verbatim: it deliberately returns null on a declaration's own name, which is exactly where find usages is invoked from.
  • Search scope from visibility - local/private searches the containing file, internal its module, public/protected the module plus its transitive dependents. The ticket's three scopes fall out of one code path.
  • Cheap prefilter - candidate files are narrowed by a word-boundary text scan (live buffer for open files, disk otherwise); only survivors are parsed and resolved.
  • Match set - the target plus its workspace-source supers (so a call via Base.foo counts as a usage of Derived.foo) plus a classifier's constructors (so Foo() counts as a usage of class Foo). Library supers are excluded, or an overridden toString would match every .toString() in the workspace.
  • Identity - KaSymbolPointer restored once per candidate session. KaSymbol is session-scoped, and a PSI or file+offset key breaks when the target's own file has unsaved edits.
  • Resolution goes through the Analysis API and PSI only, per ADR 0010 - there is no ReferencesSearch, PsiSearchHelper or word index in analysis-api-standalone-embeddable-for-ide.

Two changes outside lsp/kotlin

  • New AnalysisPriority.COMMAND (ADR 0011), between DIAGNOSTICS and INTERACTIVE, with supersedesSamePriority = false. INTERACTIVE means "a newer request makes me stale, discard my work", which is false for a user-invoked command. Today OrganizeImportsAction and ImplementMembersAction can be silently discarded by a completion request with no retry; go-to-def worked around it with a one-shot retry. All three migrate to the new priority.
  • IDELanguageClientImpl.showLocations read each result file in full, once per hit, on the main thread. Now one grouped streaming pass per file, off the main thread, retaining nothing. This also fixes Java find-references.

Known limitations (documented, to be ticketed separately)

  • Usages in src/test/** and src/androidTest/** are invisible - AndroidModule.getSourceDirectories() returns mainSourceSet only, so test sources aren't LSP content roots for any feature.
  • Java call sites of a Kotlin declaration aren't searched (the Java server has its own find-references). Kotlin call sites of a Java declaration do work.
  • Usages reached only via a subclass need a workspace inheritor search; DirectInheritorsProvider.computeIndex() rebuilds its whole index per call.
  • Binary/library symbols remain unreachable, as with go-to-def.
  • Convention references (a + b, by, destructuring) are valid entry points but are not reported as results.

Tests

:lsp:kotlin:testV7DebugUnitTest and :app:testV7DebugUnitTest:

  • TargetAtCaretTest (13) - PSI only, no session; includes the case where the caret referenceAtCaret rejects still yields a target.
  • FindUsagesTest (20) - the lib + app(dependsOn = lib) fixture from ADFA-4823: three resolution scopes, each row of the visibility ladder, super-walk and workspace-boundary cutoff, constructor expansion, imports, a Java-source target, a same-named decoy, ordering, property reads/writes, a stdlib reference, a caret naming nothing, a pre-cancelled request.
  • FindUsagesLiveDocumentTest (2) - a usage only in an unsaved buffer is found; one deleted in the buffer but still on disk is not.
  • AnalysisSerializationTest (+5) - COMMAND's ordering properties and retryingOnPreemption's one-retry contract.
  • SearchResultGroupingTest (10, :app) - multi-line hits, a hit past EOF, a column past its line's end, an unreadable file, several hits in one file from one read.
  • ReferenceAtCaretTest kept as-is, as proof that loosening visibility changed no behaviour.

Not unit-testable, so covered by the "Steps to QA" on the ticket: the menu item and its tooltip tag, the panel with a large result set, cancelling mid-search, and typing during a search without losing it.

Review

Seven commits, reviewable in order - docs/ADR first, then the COMMAND priority, TargetAtCaret, FindUsages, the menu item, and the showLocations rewrite.

… ADR

Requirements, glossary and design for find usages in the K2 Kotlin LSP,
ahead of the implementation. Follows the shape ADFA-4823 established for
go-to-definition.

Two decisions here reach outside lsp/kotlin and are recorded as such:

- ADR 0011 adds AnalysisPriority.COMMAND between DIAGNOSTICS and
  INTERACTIVE. INTERACTIVE means "a newer request makes me stale, discard
  my work", which is false for a command the user invoked and is watching.
  Organize-imports and implement-members can be silently discarded by a
  completion request today, with no retry.
- showLocations reads each result file in full once per hit, on the main
  thread. Find usages makes that a real cost rather than a latent one.

Also corrects go-to-definition's claim that ADFA-4824 would reuse
referenceAtCaret verbatim. It cannot: that helper deliberately resolves
nothing when the caret is on a declaration's own name, which is exactly
where find usages is invoked from.
INTERACTIVE means "a newer request of the same priority makes me stale, so
discard my work". That is right for completion and signature help, which
fire on keystrokes. It is wrong for a command the user invoked from the
code-actions menu and is watching a progress flashbar for: the request is
not stale, so discarding it produces a wrong answer rather than no answer.

Three commands ran at INTERACTIVE anyway. Go-to-definition noticed and
worked around it with a one-shot retry. Organize-imports and
implement-members did not: a completion request discards them, the
AnalysisPreemptedException lands in their outer runCatching, and the action
silently does nothing.

Adds COMMAND between DIAGNOSTICS and INTERACTIVE with
supersedesSamePriority = false, so two commands never discard each other,
and migrates all three actions to it. Ordered below INTERACTIVE
deliberately: a long command must not starve the completion popup, which on
a phone is part of how text gets entered. See ADR 0011 for the rejected
alternative of ordering it above.

The cost of that ordering is that commands stay preemptable, so each one
retries. Extracts retryingOnPreemption to hold the two invariants that
retry depends on: a fresh ScheduledCancelChecker per attempt (preempt()
latches, so a reused checker aborts the retry at its first checkpoint), and
re-fetching the KtFile inside the attempt (the preemptor also refreshed the
live PSI, unregistering the file the previous attempt held).

The two migrated actions now take the delegate ICancelChecker rather than a
pre-wrapped ScheduledCancelChecker, since the wrapping is per attempt.

Prep for find usages, which is the case that makes this acute: it is
user-invoked, takes one session per candidate file, and can run for
seconds, so on INTERACTIVE a single keystroke would discard it.
Find usages is invoked from either end: on a declaration's own name, or on
any reference to it. Go-to-definition's referenceAtCaret cannot serve the
first case, and not by accident - it is built so a caret on a declaration's
own name resolves nothing, which is its no-self-jump rule. That is exactly
the caret find usages starts from.

targetAtCaret is declaration-first, falling back to referenceAtCaret. It
returns a CaretTarget rather than a bare KtElement so the resolution step
does not have to re-derive which case it is looking at.

Two details worth naming:

- The declaration check requires the caret's leaf to *be* the declaration's
  name identifier, not merely to sit inside a declaration. Every caret has
  an enclosing declaration - a call site's nearest one is the function
  containing it - so proximity alone would target that container for every
  reference in the file.
- It checks both the leaf at the offset and the one before it. referenceAtCaret
  retries only when the primary leaf names nothing, which is not enough here:
  a caret just past `fun target` lands on '(', navigable in its own right for
  the invoke convention, so checking only that leaf made a caret one character
  past a declaration's name find nothing. Caught by the test for it.

Declaration-first is observable on a destructuring entry, which is both a
declaration and a convention reference: `x` in `val (x, y) = p` targets the
local x here, while go-to-definition navigates from that same caret to
component1. Deliberate, and asserted in both test classes.

navigableLeafAt becomes internal so the accept-list is shared rather than
duplicated. ReferenceAtCaret's behaviour is unchanged, and its tests are
kept as the proof of that.
Fills in KotlinLanguageServer.findReferences, which until now answered empty.
There is no reference-search infrastructure to build on: the bundled
analysis-api-standalone jar ships no ReferencesSearch, no PsiSearchHelper and
no word index, and KtFileMetadata records declarations only. So the search is
target -> match set -> scope -> candidate files -> resolve.

Match set (R3). The target, plus its workspace-source supers, plus a
classifier's constructors. Supers because a call dispatched through Base.foo
may reach Derived.foo. Constructors because Foo() resolves to a constructor,
not to the class, so without them a search on `class Foo` misses every
instantiation. The up-walk stops at the workspace boundary: with Any.toString
in the match set, a search on an overridden toString would report every
.toString() call in the workspace. Both sides of every comparison are
normalised through fakeOverrideOriginal, since a call through a subtype that
does not redeclare the member resolves to a substituted fake override.

Scope (R4) comes from the target's visibility, which is an exact bound rather
than a heuristic. local/private stays in the file, internal in the module,
anything more visible reaches the module and its transitive dependents. The
ticket's three resolution scopes fall out of this rather than being three
implementations, and a search on a local variable never leaves the open file.

Candidates (R5) are narrowed by StringSearch.containsWord, which already reads
an open file's live editor buffer rather than its saved bytes - so a usage
typed but not yet saved is still found. That matters more here than for
go-to-definition: find usages is run *while* editing. The name filter is also
what implements "convention references are not results": `a + b` contains no
plus token, so it is never a candidate.

Identity (R6) uses KaSymbolPointer, restored once per candidate session, then
compared with ==. KaSymbol cannot cross a session boundary, and KaSymbol
equality within one session compares the underlying FIR symbol, so both sides
must come from the same session. Neither PSI identity nor a (file, offset) key
would work: the live and on-disk instances of the target's own file disagree
about offsets as soon as there are unsaved edits, which would silently drop
every cross-file usage in the common case. A pointer that will not restore
drops that file rather than falling back to a looser comparison - under-report,
never report something false.

Scheduling (R9) is per candidate file: one analysis session and one
project.read each, so a preemption costs one file and index refresh is never
blocked for the length of a search. The live-PSI await stays outside
project.read, since the refresh it waits on needs project.write.

Tests cover the three resolution scopes, each row of the visibility ladder,
the super-walk and its workspace cutoff, constructor expansion, imports, a
Java-source target, a symbol-vs-name decoy, ordering, cancellation, and a
usage that exists only in an unsaved buffer.

Java files are not searched for usages, and neither are test source sets -
AndroidModule.getSourceDirectories() returns mainSourceSet only, so test
sources are not content roots for any LSP feature. Both documented in
docs/features/kotlin-find-usages.md.
Mirrors Java's action and its menu position, immediately after Go to
definition. The work itself is the editor's existing cancellable request, so
the action only starts it.

Carries its own tooltip tag rather than reusing Java's, following the split
established for Kotlin go-to-definition and fix-imports, so the two languages
can describe different behaviour. The tooltips database lives outside this
repo, so the tag shows no text until a row exists for it - that row is a
hand-off item, not code.

Deliberately always visible for .kt/.kts and never conditioned on what the
caret sits on: answering that needs PSI and the project read lock, and
prepare() runs on the UI thread. A caret on whitespace therefore shows the
item and flashes "no references". A .kts shows it and it does nothing, since a
script has no CompilationEnvironment - both identical to go-to-definition.
showLocations read every result file in full, once per hit, on the main
thread: a file with twelve usages was read and materialised twelve times,
plus an exists() stat per hit. Java find-references has had this all along
and simply rarely produces enough hits to hurt. Find usages does.

A row needs only two short strings per hit - the hit's line and the matched
text - so the fix is to group by file and read only the lines the hits touch:

- One sequential BufferedReader pass per file, stopping after the last
  wanted line, retaining nothing. Reads drop from O(hits) to O(files) and
  peak memory is one line rather than one file. Deliberately not a per-file
  content cache, which would fix the repeated reads but hold every result
  file's text at once - the wrong trade on a phone.
- The disk pass runs off the main thread via TaskExecutor, which posts the
  callback back to the UI thread.
- A file with an open editor is still resolved on the UI thread. Its Content
  is live UI state that a background thread must not touch, and pulling a few
  lines out of it is substring work with no I/O. This is also what keeps
  unsaved edits reflected in the panel.

The grouping and line extraction are extracted into SearchResultGrouping so
they can be unit-tested; the activity call stays a thin shell.

Two behaviour changes, both improvements: a hit whose line no longer exists
is dropped rather than yielding whatever Content returned, and a file whose
every hit is stale is omitted rather than contributing an empty group. The
per-hit exists() check is gone because an unreadable file now yields no lines
and therefore no rows.
Corrections found while implementing:

- The prefilter needs no live-buffer branch of its own. StringSearch.containsWord
  already reads FileManager.getActiveDocument when the file is open. Records its
  two pre-existing limits too: it reads only the first 1 MB of a file, and it
  scans through one shared unsynchronised static buffer, so a concurrent Java
  find-references can corrupt the scan.
- showLocations does not build the whole map off the main thread. A file with an
  open editor is resolved on the UI thread deliberately, because its Content is
  live UI state a background thread must not touch.
- Only KtSimpleNameExpressions are examined, so a KDoc [link] to the target is
  not reported. Added to R5 and to the non-goals rather than left implicit.
- An ambiguous reference at the caret searches its first resolved candidate.
- targetAtCaret checks both candidate leaves, not one.
- planAt/SearchPlan/candidateFiles are internal so the visibility ladder can be
  asserted; it is not observable from a result set, because symbol matching means
  a same-named decoy can never be a false positive whatever the scope.

Also updates the verification section to the tests that now exist and their
counts.
@itsaky-adfa itsaky-adfa self-assigned this Aug 4, 2026
@itsaky-adfa
itsaky-adfa requested a review from a team August 4, 2026 11:39

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1a24f09-7f07-4c6c-aef9-73d095945bba

📥 Commits

Reviewing files that changed from the base of the PR and between 522fd47 and 3abd405.

📒 Files selected for processing (1)
  • idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt

📝 Walkthrough
  • Added Kotlin K2 LSP find-usages support with visibility-based scopes, live-document support, cross-module resolution, cancellation, deduplication, and sorted results.
  • Added the Kotlin “Find References” editor action and tooltip metadata.
  • Added AnalysisPriority.COMMAND and preemption retry handling for user-invoked analysis actions.
  • Improved search-result grouping by reading each file once and moving disk I/O off the UI thread.
  • Fixed dependent-module mapping when multiple modules share dependencies.
  • Added ADRs, feature documentation, known limitations, and unit and integration tests.
  • Risk: Analysis API or PSI resolution failures can produce incomplete results for unresolved, unsupported, or non-workspace symbols.
  • Risk: Word-boundary candidate filtering can miss usages when source syntax does not match the expected symbol name.
  • Risk: Asynchronous disk reads and live-document merging require careful stale-result and lifecycle handling.
  • Risk: Broad failure isolation can hide individual analysis failures to preserve on-device resilience.
  • Manual verification remains required for showLocations staleness handling and related UI behavior.

Walkthrough

Adds Kotlin K2 find-usages navigation with declaration-aware caret resolution, visibility-based scopes, live-buffer support, grouped result rendering, command-priority analysis retries, and a Kotlin editor action.

Changes

Kotlin navigation and search results

Layer / File(s) Summary
Caret target and search planning
lsp/kotlin/.../navigation/*, lsp/kotlin/.../navigation/TargetAtCaretTest.kt, docs/features/kotlin-goto-definition.md
Caret resolution distinguishes declarations from references and supports usage-search planning.
Usage search engine and server flow
lsp/kotlin/.../navigation/FindUsages.kt, KotlinLanguageServer.kt, ModuleDependentsProvider.kt, lsp/kotlin/src/test/.../navigation/*, docs/features/kotlin-find-usages.md, docs/adr/0010-navigation-resolves-via-analysis-api.md
Find-usages resolves symbols, derives visibility scopes, searches Kotlin sources and live buffers, handles overrides and constructors, and returns sorted locations.
Command-priority analysis and retries
lsp/kotlin/.../compiler/modules/AnalysisScheduler.kt, actions/*, navigation/GoToDefinition.kt, lsp/kotlin/src/test/.../compiler/modules/*, docs/adr/0011-command-analysis-priority.md, docs/adr/README.md
Analysis adds the COMMAND priority and retries preempted command work once with fresh cancellation state.
Find-references editor integration
lsp/kotlin/.../KotlinCodeActionsMenu.kt, actions/FindReferencesAction.kt, idetooltips/.../TooltipTag.kt, lsp/kotlin/src/test/.../KotlinCodeActionTooltipTagTest.kt
The Kotlin menu exposes a UI-thread find-references action with tooltip metadata and editor delegation.
Grouped search-result rendering
app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt, IDELanguageClientImpl.java, app/src/test/.../SearchResultGroupingTest.kt
Locations are grouped by file. Live editor content and bounded disk reads are merged before display, with stale rows omitted.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor as Kotlin editor
  participant Action as FindReferencesAction
  participant Server as KotlinLanguageServer
  participant Search as findUsagesAt
  participant Files as Source files and live buffers
  participant Client as IDELanguageClientImpl

  Editor->>Action: execute find-references
  Action->>Server: request findReferences
  Server->>Search: resolve target and search scope
  Search->>Files: inspect candidate content
  Files-->>Search: matching locations
  Search-->>Server: sorted locations
  Server-->>Client: reference locations
  Client->>Files: group and read result content
  Client-->>Editor: publish search results
Loading

Possibly related PRs

Suggested reviewers: dara-abijo-adfa, jatezzz

Poem

A rabbit hops through Kotlin’s tree,
Finding each reference carefully.
Live buffers join the files in flight,
Commands retry when preempted right.
Grouped results bloom in sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: Kotlin K2 LSP find-usages support.
Description check ✅ Passed The description directly explains the Kotlin find-usages implementation, related scheduler and UI changes, documentation, limitations, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree/ADFA-4824

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt (1)

76-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep cancellation and second preemption out of the generic getOrElse fallback.

runCatching catches every Throwable; after retryingOnPreemption handles one AnalysisPreemptedException, a second preemption and CancellationException from createJobCancelChecker() still fall into .getOrElse { ... emptyList() }. Use typed handling that rethrows CancellationException and preserves the second preemption as an explicit result. Apply the same handling in ImplementMembersAction.kt:76-102 and OrganizeImportsAction.kt:63-82.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt`
around lines 76 - 102, Replace the broad runCatching/getOrElse handling in
ImplementMembersAction.kt lines 76-102 and OrganizeImportsAction.kt lines 63-82
with typed exception handling: rethrow CancellationException, preserve a second
AnalysisPreemptedException as an explicit result, and use the generic fallback
only for other failures. Keep retryingOnPreemption behavior unchanged.

Sources: Coding guidelines, Learnings

🧹 Nitpick comments (2)
app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt (1)

49-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the live-buffer overload.

The tests exercise the Map<Int, String> overload and readLines, but the resultsFor(file, locations, content: Content) path has separate out-of-range filtering. Add one JVM test that passes a small Content("only") for Stale.kt with a stale hit (for example file, listOf(location(file, 1, 0, 1, 3)), Content("only")) and asserts the result is empty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt`
around lines 49 - 60, Add a JVM test for the Content overload of
SearchResultGrouping.resultsFor, using Stale.kt, a stale hit on line 1, and
Content("only"). Assert that the returned results are empty, covering
out-of-range filtering for live-buffer content.
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt (1)

10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use JUnit Jupiter in the Kotlin lsp tests.

These new test classes import JUnit 4 annotations. The test guideline requires JUnit Jupiter for **/src/test/*.{kt,java}, so migrate the annotations and the @After lifecycle hook. KtLspTest, KtLspTestRule, and the Robolectric runner currently use JUnit 4-only APIs, so move those to JUnit/Jupiter-compatible fixtures or obtain an approved legacy exception before keeping these imports.

  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt#L10: Replace org.junit.Test with org.junit.jupiter.api.Test.
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesLiveDocumentTest.kt#L12-L13: Replace org.junit.After with org.junit.jupiter.api.AfterEach and move the org.junit.Test import to Jupiter.
  • Also migrate KtLspTest/KtLspTestRule if new tests continue to depend on @Rule, @RunWith, TestRule, Statement, or TemporaryFolder.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt`
at line 10, Migrate the Kotlin LSP tests to JUnit Jupiter: in FindUsagesTest.kt
replace the JUnit 4 Test import, and in FindUsagesLiveDocumentTest.kt replace
Test and After with Jupiter Test and AfterEach. Update KtLspTest and
KtLspTestRule to remove JUnit 4-only `@Rule`, `@RunWith`, TestRule, Statement, and
TemporaryFolder dependencies, or obtain an approved legacy exception before
retaining them.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/0011-command-analysis-priority.md`:
- Around line 31-33: Update the priority-order code fence containing “INDEXING <
DIAGNOSTICS < COMMAND < INTERACTIVE” to use the text language label, preserving
its plain-text rendering and resolving the MD040 warning.

In `@docs/features/kotlin-find-usages.md`:
- Line 201: Specify the diagram’s fenced code block language as text by changing
the untyped Markdown fence associated with the pseudo-code diagram; leave the
diagram content unchanged.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt`:
- Around line 81-95: Update the retry flow around retryingOnPreemption and
findEnclosingClassOrObject so it does not reuse the captured offset after the
live PSI changes. Bind the request to the document revision and target marker,
then re-resolve the original target on retry or reject the result as stale
before postExec; add a regression test that inserts text before the caret during
the first attempt.

In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt`:
- Line 426: Update the tests in AnalysisSerializationTest at the affected
methods to replace JUnit 4 `@Test`(timeout = ...) usage with JUnit Jupiter
`@Timeout`, including the required Jupiter import, while preserving each existing
timeout duration and test behavior.
- Around line 432-459: Update the first thread in the withAnalysisLock test to
poll holderChecker.abortIfCancelled() while waiting on release, allowing
same-priority preemption to be observed; then assert that the first command was
not preempted in addition to the existing entry assertions. Preserve the current
synchronization and release flow.

---

Outside diff comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt`:
- Around line 76-102: Replace the broad runCatching/getOrElse handling in
ImplementMembersAction.kt lines 76-102 and OrganizeImportsAction.kt lines 63-82
with typed exception handling: rethrow CancellationException, preserve a second
AnalysisPreemptedException as an explicit result, and use the generic fallback
only for other failures. Keep retryingOnPreemption behavior unchanged.

---

Nitpick comments:
In `@app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt`:
- Around line 49-60: Add a JVM test for the Content overload of
SearchResultGrouping.resultsFor, using Stale.kt, a stale hit on line 1, and
Content("only"). Assert that the returned results are empty, covering
out-of-range filtering for live-buffer content.

In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt`:
- Line 10: Migrate the Kotlin LSP tests to JUnit Jupiter: in FindUsagesTest.kt
replace the JUnit 4 Test import, and in FindUsagesLiveDocumentTest.kt replace
Test and After with Jupiter Test and AfterEach. Update KtLspTest and
KtLspTestRule to remove JUnit 4-only `@Rule`, `@RunWith`, TestRule, Statement, and
TemporaryFolder dependencies, or obtain an approved legacy exception before
retaining them.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b6f968a-a074-4ba3-8584-c84c4c25b501

📥 Commits

Reviewing files that changed from the base of the PR and between 5305e52 and fdfd390.

📒 Files selected for processing (26)
  • app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java
  • app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt
  • app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt
  • docs/adr/0010-navigation-resolves-via-analysis-api.md
  • docs/adr/0011-command-analysis-priority.md
  • docs/adr/README.md
  • docs/features/kotlin-find-usages.md
  • docs/features/kotlin-goto-definition.md
  • idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/FindReferencesAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/TargetAtCaret.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionTooltipTagTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesLiveDocumentTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/TargetAtCaretTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/ImplementMembersEndToEndTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/OrganizeImportsEndToEndTest.kt

Comment thread docs/adr/0011-command-analysis-priority.md Outdated
Comment thread docs/features/kotlin-find-usages.md Outdated
The direct- and refinement-dependents maps were built per module and merged
with `reduce { acc, value -> acc + value }`. `Map + Map` *replaces* a shared
dependency's dependent set rather than merging it, so a module used by more
than one other kept only the last of them.

Find usages reads that map for R4's public-visibility scope, so a public
declaration in a module with two consumers silently reported no usages in all
but one of them - and `reduce` additionally threw on an empty module list.

Accumulate into one map across all modules instead.
Five fixes to the search itself, all of them cases where it answered "no
references" for a symbol with plenty, or did far more work than it needed to:

- A candidate file preempted twice escaped `retryingOnPreemption` as a
  `CancellationException` and unwound the whole search, discarding every
  location already collected. Preemption is keystroke-driven work winning the
  lock, not the user cancelling, so it now costs that file like any other
  candidate failure. Genuine cancellation still propagates.

- The declaration path came from the VFS alone, but the file the user is
  editing is a live `KtFile` whose `virtualFile` is a `LightVirtualFile`. A
  local or `private` target therefore had no path in the common case, fell
  through R4's single-file scope and searched the whole module graph for a
  variable that cannot leave one block. Derive it through `backingFilePath`
  first, as go-to-definition does, and fall back to module scope - never to
  the dependents graph - when there is still no path.

- The text prefilter went through `StringSearch.containsWord`, which reads
  only a file's first megabyte (silently dropping usages below the mark),
  reads through one process-global `ByteBuffer` the Java server mutates from
  its own threads, and rethrows an unreadable file as a `RuntimeException`,
  which aborted the entire search. Replaced with `mentionsName`: whole file,
  line by line through `FileManager.getReader`, so an open file is still
  matched against its live buffer, and an unreadable one drops out with a log.

- The prefilter had no cancel checker, so cancelling mid-scan let it read
  every remaining source file before the result was discarded. It now checks
  per file.

- Every prefiltered candidate paid an analysis-lock acquisition, a FIR session
  and a match-set restore before the pure-PSI name filter could reject it. On
  a short, common name most candidates only mention it in a comment or a
  string literal. Run the name filter first and skip the session entirely when
  it finds nothing.
Moving the result-file reads off the main thread made the publish
asynchronous, but nothing checked that the request still owned the panel. Two
overlapping searches published in completion order, not request order, so a
slow find-references that started first landed last and overwrote the newer
search the user was looking at - and rows they never asked for navigated
somewhere unrelated when tapped.

`showLocations` now claims the panel with a request counter and captures
`EditorViewModel.currentSearchGeneration`; the callback publishes only if both
still hold. The counter catches a superseding find-references, the generation
catches a text search publishing in between.

Panel visibility moves next to the rows in `publishLocations` for the same
reason: it was committed eagerly while the publish could be skipped entirely
(activity recreated mid-read), which left the panel open with the "no results"
placeholder hidden over the previous query's rows.
R4 gains the path-derivation fallback, R5 the new prefilter and its
cancellation granularity, R6 the PSI-before-session ordering, R9 the
twice-preempted candidate, R10 the panel's staleness guard and R12 the two
failures now isolated per file. The flow diagram and the touched-components
list follow.

Also drops two claims that were never true of the shipped code: that locals
skip the pointer machinery, and that the `StringSearch` limits were carried
over unfixed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt (1)

112-119: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not catch JVM errors as recoverable failures.

The request handler at lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt:112 catches Throwable, so OutOfMemoryError, StackOverflowError, and linkage errors are logged as usage-search failures and can return an empty result. Narrow this to the recoverable analysis, PSI, and I/O exceptions, and let Error propagate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt`
around lines 112 - 119, Narrow the catch in the usage-search request handler
around the cancellation check and failure log to recoverable analysis, PSI, and
I/O exceptions, allowing JVM Error types to propagate instead of returning an
empty result. Apply the same exception-boundary correction to the sibling
handling at
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt
lines 441-445; both sites are within FindUsages request handling and must no
longer catch Throwable.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt`:
- Around line 112-119: Narrow the catch in the usage-search request handler
around the cancellation check and failure log to recoverable analysis, PSI, and
I/O exceptions, allowing JVM Error types to propagate instead of returning an
empty result. Apply the same exception-boundary correction to the sibling
handling at
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt
lines 441-445; both sites are within FindUsages request handling and must no
longer catch Throwable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 141966a0-37c4-4cd3-b2c6-36000272faae

📥 Commits

Reviewing files that changed from the base of the PR and between fdfd390 and 8ea969d.

📒 Files selected for processing (4)
  • app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java
  • docs/features/kotlin-find-usages.md
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt

@jatezzz jatezzz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff plus the surrounding code it depends on (AnalysisScheduler/KtFileExts, IDEEditor.onFindReferencesResult, EditorViewModel.currentSearchGeneration, TaskExecutor, FileManager, AbstractSourceModule.computeFiles, KtSymbolIndex.getKtFile). I did not build or run the tests — everything below is by reading.

Assessment

Unusually strong. The ADR earns its place (the COMMAND-above-INTERACTIVE alternative is rejected for the right reason), per-candidate-file lock granularity is the load-bearing design choice and it's argued rather than asserted, and two genuine pre-existing bugs get fixed on the way: ModuleDependentsProvider's Map + Map merge was replacing a shared dependency's dependent set, and the search panel was doing main-thread I/O once per hit.

The showLocations staleness guard correctly needs both mechanisms it uses — the request counter for two showLocations racing, the ViewModel generation for a project-search publish landing in between. And the per-file ScheduledCancelChecker churn doesn't leak listeners on the request-scoped delegate, because withAnalysisLock removes its listener in finally.

Findings

1. Super-dispatched usages in a supertype's module are never searched

FindUsages.kt, R3/R4 interaction.

The match set walks up to workspace supers, so base.foo() counts as a usage of Derived.foo. But scopeOf derives the scope from the target's module plus its dependents only. Put Base in lib and Derived in app: a base.foo() call written in lib is a real usage that will never be looked at, because lib is a dependency of app, not a dependent.

The test that covers this case (a call dispatched through a workspace supertype is a usage of the override) puts both types in module app, so it can't catch it.

Either union the modules of every match-set member's declaration into the scope, or add it to Known Limitations — as written, the doc reads as though the super-walk is complete.

2. A double preemption during target resolution silently reports "no references"

FindUsages.kt, planAt / findUsagesAt.

planAt wraps in retryingOnPreemption. If the second attempt is also preempted, AnalysisPreemptedException escapes into findUsagesAt's catch (e: Throwable), where isAnalysisCancellation() is true (it is a CancellationException) and the result becomes ReferenceResult.empty(). The user's coroutine is still alive, so onFindReferencesResult flashes "No references found" for a symbol with plenty — precisely the failure mode ADR 0011 exists to eliminate.

usagesIn gets this right with a dedicated catch (e: AnalysisPreemptedException) ahead of the generic one; findUsagesAt should distinguish preemption from cancellation the same way. Rare (two preemptions inside one short plan phase), but the point of the new priority is that this class of silent wrong answer stops happening.

3. .java candidates are fully read by the prefilter, then guaranteed to be discarded

FindUsages.kt, candidateFiles.

computeFiles yields .kt and .java (AbstractSourceModule.kt:23). Every Java file in scope therefore gets a full line-by-line mentionsName read, and any that survives is dropped moments later by ktFileFor -> getKtFile -> isKotlinFile (KtSymbolIndex.kt:279).

Searching .java is explicitly out of scope, so filtering the extension before the read is free. On a Java-heavy workspace this is a large fraction of the prefilter's I/O spent on guaranteed-zero results — and the prefilter is the part of the search the user waits on.

4. namedReferences materialises every simple-name expression per candidate file

FindUsages.kt.

PsiTreeUtil.collectElementsOfType(ktFile, KtSimpleNameExpression::class.java).filter { … } allocates the full list before filtering, and the walk polls no cancel checker. A PsiRecursiveElementWalkingVisitor filtering on getReferencedName() inline drops the intermediate collection and gives cancellation a checkpoint inside the walk. Matters most in the case the prefilter is worst at: a short, common name in a large file.

Nits

  • kotlin-find-usages.md R12 says "Genuine cancellation propagates rather than being reported as 'no references'", but findUsagesAt converts every isAnalysisCancellation() into empty. Harmless in practice (a cancelled coroutine never reaches onFindReferencesResult), but the doc states the opposite of the code.
  • @PublishedApi on schedulerLogger is unnecessary — retryingOnPreemption is internal inline, not public inline, so it may reference internal top-level declarations directly. The annotation only widens the property's bytecode visibility.
  • SearchResultGrouping.resultsFor: val startLine = lines[range.start.line] names a String as though it were a line number, and matchedText immediately looks the same key up again. lineText, passed in, reads better.
  • The design section says candidate files are selected "with no lock at all"; computeFiles takes project.read per file (AbstractSourceModule.kt:25). The real claim — no lock held across the search — is intact; the wording isn't.

Test coverage

45 new/changed tests, testing the right things: the visibility ladder is asserted on SearchPlan.scope rather than inferred from results, the toString-override case pins the workspace-boundary cutoff, FindUsagesLiveDocumentTest isolates the enableParserEventSystem requirement, and ReferenceAtCaretTest is deliberately left untouched as the proof that loosening navigableLeafAt to internal changed nothing.

SearchResultGroupingTest avoids Content entirely so it stays a plain JVM test — worth noting the Content overload is consequently unexercised, as is the showLocations staleness guard.

Gap tied to finding 1: a cross-module super/override fixture (Base in lib, Derived in app) would have surfaced it.

Security / conventions

Nothing security-relevant — no new dependencies, no network, no new permissions, all I/O inside the workspace. Tabs, comment discipline, and ASCII-in-code all conform; the commits are ordered docs -> priority -> helper -> feature -> menu -> panel as the description claims.

Recommendation

Good to merge once finding 2 (small, contained) and finding 3 (one-line filter) are addressed. Finding 1 needs either a fix or a line in Known Limitations before merge; finding 4 is fine as a follow-up.


🤖 Review generated with Claude Code

The match set walks up to workspace supers, so `base.paint()` counts as a
usage of `Derived.paint`. The scope did not follow: it was the target's module
plus its transitive *dependents*, so with `Base` in `lib` and `Derived` in
`app` a call written in `lib` was never looked at - `lib` is a dependency of
`app`, not a dependent.

Union every match-set member's module (and its dependents) into the scope.
Library supers are already out of the match set, so the union cannot leave the
workspace, and the file-confined and `internal` rows need no widening: private
cannot override, and this project model has no friend modules.

The existing super-dispatch test put both types in `app`, so it could not
catch this; the two new tests fail without the union.
`planAt` retries a preemption once, and a second one escaped as an
`AnalysisPreemptedException` - a `CancellationException`, so `findUsagesAt`'s
cancellation branch swallowed it and returned empty. The user's coroutine is
still alive there, so the editor flashed "No references found" for a symbol
with plenty: the exact wrong answer ADR 0011 exists to prevent. `usagesIn`
already separates the two per candidate file.

Run the plan phase twice over, then give up with a warning instead of a lie.
It is one file and one short session, so the extra attempts are cheap.
Genuine cancellation still short-circuits - the delegate throws a plain
`CancellationException`, not this subtype.
A source module's files are .kt *and* .java. Every Java file in scope got a
full line-by-line prefilter read, and any that mentioned the name was dropped
moments later by `ktFileFor` - searching .java is a non-goal. Filter on the
extension before the read: on a Java-heavy workspace that is a large share of
the prefilter's I/O, which is the part the user waits on.

`namedReferences` also collected every KtSimpleNameExpression in the file
before filtering, with no cancellation checkpoint inside the walk. Filter
during the walk instead - it matters exactly where the text prefilter is
weakest, a short common name in a large file.
- Drop `@PublishedApi` from `schedulerLogger`: `retryingOnPreemption` is
  internal inline, not public inline, so it can reference an internal
  top-level declaration directly. The annotation only widened the property's
  bytecode visibility.
- `SearchResultGrouping` named a line's *text* `startLine` and then re-looked
  it up inside `matchedText`. Name it `lineText` and pass it in, along with the
  Range it was already destructuring by hand.
- The prefilter does not run lock-free: `computeFiles` takes `project.read`
  per file. Say what is actually true - nothing is held *across* the pass.
- Label the two plain-text code fences (markdownlint MD040).
- Both Kotlin actions warn-logged a cancellation or a second preemption as
  "Failed to ...". Log those at debug so the real failures stay visible.
`a command does not supersede an in-flight command` passed either way. The
holder blocked in `release.await()` and never polled its checker, and
preemption is cooperative, so with `supersedesSamePriority` wrongly on for
COMMAND the holder kept the lock, the second command still could not enter
before the release, and the entry assertions still held. Poll the checker while
waiting and assert the holder was not preempted - the test now fails when the
flag is flipped.

`SearchResultGrouping.resultsFor(file, locations, Content)` was unexercised,
including its own out-of-range filter. Cover both the buffer-wins-over-disk
path and a stale hit; sora's Content needs no Robolectric runner, so the class
stays a plain JVM test.
@itsaky-adfa

Copy link
Copy Markdown
Contributor Author

Review responses

Thanks both. Every item was checked against the code; five commits pushed (6cf753c4c..522fd4758), and the rejections are argued rather than skipped.

lsp:kotlin 259 tests / app 163 tests, 0 failures. Both correctness fixes are red-then-green: the two new cross-module tests fail without the scope union, and the hardened scheduler test fails when COMMAND.supersedesSamePriority is flipped to true (it passed either way before).

@jatezzz

1. Super-dispatched usages in a supertype's module — fixed (6cf753c4c)

Correct, and the diagnosis was exact. scopeOf now unions every match-set member's module (plus that module's dependents) instead of only the target's, so Base in lib / Derived in app searches lib. Library supers are already filtered out of the match set, so the union cannot leave the workspace, and the file-confined and internal rows keep their early returns — private cannot override, and there are no friend modules here, so internal cannot be overridden across one.

Both new tests are the fixture you described (Base + call site in lib, Derived in app); they fail without the union. R3, R4 and acceptance criterion 11 updated.

2. Double preemption during target resolution — fixed (34ce596f2)

Confirmed by reading: retryingOnPreemption propagates the second AnalysisPreemptedException, it is a CancellationException, so findUsagesAt's cancellation branch returned empty and the editor flashed "No references found". The plan phase now runs planAt twice over (four underlying attempts — it is one file and one short session) and gives up with a warning rather than a lie. Genuine cancellation still short-circuits, since the delegate throws a plain CancellationException.

A distinct "search interrupted" message would be better still, but ReferenceResult has no failure field and a safeGet null already lands on the same flash, so it needs a new result state plus a string — not in this PR.

3. .java candidates read then discarded — fixed (a67280e9f)

One-line extension filter before mentionsName, matching what DocumentUtils.isKotlinFile accepts downstream. R5 says so now.

4. namedReferences materialises every simple-name expression — fixed (a67280e9f)

Taken now rather than as a follow-up, since it was small: a PsiRecursiveElementWalkingVisitor filtering on getReferencedName() inline, with abortIfCancelled() per element.

Nits — all four applied (7a4024576)

  • R12 reworded: it claimed cancellation propagates when findUsagesAt converts it to empty. It now says what the code does, and why preemption and cancellation must not share a handler.
  • @PublishedApi dropped from schedulerLogger — right, internal inline is not public inline.
  • SearchResultGrouping: startLine -> lineText, passed into matchedText (which now takes the Range too, instead of four unpacked ints).
  • The "no lock at all" wording corrected in both the doc and the findUsagesAt kdoc: computeFiles does take project.read per file; the real claim is that nothing is held across the pass.

Coverage gaps you flagged — the cross-module fixture is in (finding 1), and the Content overload is now tested (522fd4758). showLocations' staleness guard is still untested; it needs the editor/UI, so it stays a manual check.

@coderabbitai

Applied

  • MD040 on both fences (7a4024576). Note the "committable suggestion" blocks were empty, so they were applied by hand.
  • The same-priority preemption test (522fd4758) — this was the best catch of the review. The holder blocked in release.await() and never polled its checker, and preemption is cooperative, so with the flag wrongly on the holder kept the lock, the second command still could not enter before the release, and the entry assertions still held. Now polls and asserts firstPreempted is false; it fails when the flag is flipped.
  • The Content-overload test (522fd4758). It needs no Robolectric runner, so the class stays a plain JVM test.

Not applied

  • JUnit Jupiter migration (@Test(timeout = ...) -> @Timeout, and FindUsagesTest/FindUsagesLiveDocumentTest): the premise is wrong. Only gradle-plugin/build.gradle.kts calls useJUnitPlatform(). app and lsp/kotlin get JUnit 4 (junit:junit:4.13.2) plus Robolectric through testing/unit, and KtLspTest/KtLspTestRule are built on @Rule/@RunWith/TemporaryFolder. Migrating these files would break the suites, not modernise them.
  • Narrowing catch (Throwable) in FindUsages so Error propagates: deliberate. On-device, logging a StackOverflowError out of deep PSI recursion and degrading one search is a better failure mode than killing the LSP thread. The genuinely harmful case inside that catch was preemption-vs-cancellation, fixed in 34ce596f2.
  • ImplementMembersAction reusing a stale caret offset after a retry: real, but pre-existing and not introduced here — the action is async, so the document can already move between caret capture and the first attempt; the retry only widens the window. The fix (bind the request to a document revision and target marker, reject a stale result before postExec) is its own change, tracked outside this PR.
  • Typed handling instead of runCatching/getOrElse in the two actions: both docblocks justify swallowing everything — the action framework only catches IllegalArgumentException and the scope has no exception handler. Rethrowing a second AnalysisPreemptedException (a CancellationException thrown while the job is not cancelled) would cancel the action silently: same user-visible no-op, more risk. What was wrong was the log level, so both now log a cancellation or preemption at debug and keep warn for real failures (7a4024576).

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== commits ==='
git log --oneline 6cf753c4c^..522fd4758

echo '=== changed files ==='
git diff --name-only 6cf753c4c^..522fd4758

echo '=== module test configuration ==='
fd -a 'build.gradle.kts' . | while IFS= read -r file; do
  if rg -n -C2 'useJUnitPlatform\(\)|junit:junit:4\.13\.2|Robolectric|testing/unit' "$file"; then
    echo "--- $file"
  fi
done

echo '=== scheduler and grouping test updates ==='
fd -a 'AnalysisSerializationTest.kt' 'SearchResultGroupingTest.kt' . -x sh -c '
  echo "--- $1"
  rg -n -C3 "firstPreempted|supersedesSamePriority|Content|resultsFor" "$1" || true
' sh {}

echo '=== relevant find-usages and action handling ==='
fd -a 'FindUsages.kt' 'ImplementMembersAction.kt' 'OrganizeImportsAction.kt' . -x sh -c '
  echo "--- $1"
  rg -n -C3 "catch \\(Throwable\\)|AnalysisPreemptedException|CancellationException|runCatching|getOrElse|scopeOf|planAt|mentionsName|namedReferences" "$1" || true
' sh {}

Length of output: 11559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

find_one() {
  fd -a -t f "$1" . | head -n 1
}

find_usages=$(find_one 'FindUsages.kt')
grouping_test=$(find_one 'SearchResultGroupingTest.kt')
implement_members=$(find_one 'ImplementMembersAction.kt')
organize_imports=$(find_one 'OrganizeImportsAction.kt')

echo '=== declarations ==='
ast-grep outline "$find_usages" --items all --type function --match 'scopeOf|namedReferences|planWithRetry'
ast-grep outline "$grouping_test" --items all --type function --match 'Content|resultsFor'
ast-grep outline "$implement_members" --items all --type function --match 'execAction|compute'
ast-grep outline "$organize_imports" --items all --type function --match 'execAction|compute'

echo '=== scope union and reference walker ==='
sed -n '302,365p;525,575p' "$find_usages"

echo '=== Content-overload coverage ==='
rg -n -C4 'Content|resultsFor' "$grouping_test"

echo '=== command actions exception handling ==='
rg -n -C5 'runCatching|getOrElse|AnalysisPreemptedException|CancellationException|logger\.(debug|warn)' \
  "$implement_members" "$organize_imports"

echo '=== test dependencies and test-platform configuration ==='
rg -n -C3 'useJUnitPlatform\(\)|junit:junit:4\.13\.2|testImplementation\(.*junit|testing/unit|Robolectric' \
  -g 'build.gradle.kts' .

echo '=== cross-module supertype coverage ==='
rg -n -C4 'supertype|Base|Derived|module.*lib|lib.*module|dispatched' \
  "$(find_one 'FindUsagesTest.kt')"

Length of output: 27725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

kotlin_organize='lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt'

echo '=== Kotlin organize-imports handling ==='
ast-grep outline "$kotlin_organize" --items all --type function --match 'execAction|organize'
rg -n -C5 'runCatching|getOrElse|AnalysisPreemptedException|CancellationException|logger\.(debug|warn)' \
  "$kotlin_organize"

echo '=== affected module dependencies ==='
for file in app/build.gradle.kts lsp/kotlin/build.gradle.kts; do
  echo "--- $file"
  rg -n -C3 'testImplementation|testing\.unit|tests\.junit|tests\.robolectric' "$file" || true
done

echo '=== version-catalog JUnit aliases ==='
fd -a -t f 'libs.versions.toml' . -x rg -n -C3 'junit|robolectric|testing' {}

Length of output: 6071


@itsaky-adfa Verified. The applied fixes address the reported findings.

  • scopeOf now includes match-set member modules and their dependents.
  • The dependency-module supertype tests verify the corrected scope.
  • namedReferences now uses a cancellation-aware PSI walk.
  • The prefilter excludes non-Kotlin files before disk reads.
  • The Content overload now has direct test coverage.
  • The scheduler test now detects incorrect same-priority preemption.
  • Both Kotlin command actions classify cancellation at debug level and other failures at warn level.

The JUnit Jupiter migration recommendation was incorrect. app and lsp/kotlin use the JUnit 4 test stack. Only gradle-plugin explicitly uses JUnit Platform and JUnit Jupiter. I withdraw that recommendation.

I accept the documented decisions for broad failure isolation in FindUsages and for deferring the pre-existing stale-caret issue. I did not run the test suites in the shell sandbox. Your reported test results remain the execution evidence.

🐇 ✅


✏️ Learnings added
Learnt from: itsaky-adfa
URL: https://github.com/appdevforall/CodeOnTheGo/pull/1624

Timestamp: 2026-08-05T10:37:01.900Z
Learning: The `app` and `lsp/kotlin` modules use the JUnit 4 test stack through the shared `testing.unit` and `testing.lsp` dependencies. The Gradle version catalog maps `libs.tests.junit` to `junit:junit:4.13.2`. Only `gradle-plugin` explicitly configures `useJUnitPlatform()` and JUnit Jupiter.

Learnt from: itsaky-adfa
URL: https://github.com/appdevforall/CodeOnTheGo/pull/1624

Timestamp: 2026-08-05T10:37:01.900Z
Learning: In `lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt`, broad failure isolation is intentional for on-device resilience. The implementation logs failures such as deep-PSI `StackOverflowError` and degrades one find-usages request rather than terminating the LSP thread. Analysis preemption must remain distinct from genuine cancellation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt (1)

98-102: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Catch only expected file-read failures.

Line 98 catches Exception and converts unexpected defects into omitted search results. Catch IOException and any other explicitly intended file-access exception instead. Let programming errors propagate.

Proposed fix
+import java.io.IOException
+
-		} catch (e: Exception) {
+		} catch (e: IOException) {

As per coding guidelines, catch recoverable I/O failures locally and avoid broad catches. Based on learnings, prefer a specific exception type instead of Exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt` around
lines 98 - 102, Update the catch block in the search-result file reading logic
to catch only IOException and other explicitly intended file-access exceptions,
rather than Exception. Preserve the existing debug logging and omitted-lines
fallback for recoverable read failures, while allowing programming errors to
propagate.

Sources: Coding guidelines, Learnings

app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt (1)

18-20: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use JUnit Jupiter temporary-directory support.

Lines 18-20 use the JUnit 4 rule model. Replace TemporaryFolder with Jupiter @TempDir. Update the JUnit 4 @Test import in this new test file at the same time.

Proposed fix
-import org.junit.Rule
-import org.junit.rules.TemporaryFolder
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Path

-	`@get`:Rule
-	val folder = TemporaryFolder()
+	`@TempDir`
+	lateinit var folder: Path

As per coding guidelines, use JUnit Jupiter for new tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt`
around lines 18 - 20, Update SearchResultGroupingTest to use JUnit Jupiter
temporary-directory support: replace the TemporaryFolder rule with a `@TempDir`
directory field and change the test annotation import from JUnit 4 to Jupiter.
Remove the obsolete TemporaryFolder and Rule usage while preserving the test’s
existing behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt`:
- Around line 98-102: Update the catch block in the search-result file reading
logic to catch only IOException and other explicitly intended file-access
exceptions, rather than Exception. Preserve the existing debug logging and
omitted-lines fallback for recoverable read failures, while allowing programming
errors to propagate.

In `@app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt`:
- Around line 18-20: Update SearchResultGroupingTest to use JUnit Jupiter
temporary-directory support: replace the TemporaryFolder rule with a `@TempDir`
directory field and change the test annotation import from JUnit 4 to Jupiter.
Remove the obsolete TemporaryFolder and Rule usage while preserving the test’s
existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2445bc5-4235-488a-b358-8c509e9c1da4

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea969d and 522fd47.

📒 Files selected for processing (10)
  • app/src/main/java/com/itsaky/androidide/lsp/SearchResultGrouping.kt
  • app/src/test/java/com/itsaky/androidide/lsp/SearchResultGroupingTest.kt
  • docs/adr/0011-command-analysis-priority.md
  • docs/features/kotlin-find-usages.md
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsages.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/navigation/FindUsagesTest.kt
💤 Files with no reviewable changes (1)
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/adr/0011-command-analysis-priority.md
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt

@jatezzz
jatezzz self-requested a review August 5, 2026 13:07
@itsaky-adfa
itsaky-adfa merged commit 26ec898 into stage Aug 11, 2026
4 checks passed
@itsaky-adfa
itsaky-adfa deleted the worktree/ADFA-4824 branch August 11, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants