Skip to content

Use SearchEngine.getSearchParticipants() for non-Java language search#3772

Open
arcivanov wants to merge 1 commit into
eclipse-jdtls:mainfrom
arcivanov:feature/search-participant-extension-point
Open

Use SearchEngine.getSearchParticipants() for non-Java language search#3772
arcivanov wants to merge 1 commit into
eclipse-jdtls:mainfrom
arcivanov:feature/search-participant-extension-point

Conversation

@arcivanov
Copy link
Copy Markdown

@arcivanov arcivanov commented May 6, 2026

Summary

  • Update all search call sites (ReferencesHandler, CodeLensHandler, ImplementationCollector, HoverInfoProvider, WorkspaceSymbolHandler) to use SearchEngine.getSearchParticipants() instead of hardcoding only the default Java participant
  • Supplement WorkspaceSymbolHandler.searchAllTypeNames() and searchAllMethodNames() with search() calls through contributed participants — both APIs only query the default Java participant's indexes, making non-Java types and methods invisible to workspace/symbol queries
  • Supplement ImplementationCollector.findTypeImplementations() with an IMPLEMENTORS search through contributed participants — newTypeHierarchy().getAllSubtypes() only discovers Java subtypes
  • Add fallback in JDTUtils.resolveCompilationUnit(IFile) to query contributed SearchParticipant.getCompilationUnit(IFile) for non-Java source files
  • Add fallback in JDTUtils.findElementsAtSelection() to search contributed participants when Java's codeSelect() returns empty — enables go-to-definition and hover for types/methods provided by non-Java languages (e.g., Kotlin facade classes and property accessors) from Java source files
  • Add type hierarchy support for contributed participants: supertype resolution via search, subtype supplementation from contributed participants, and fallback element re-resolution when JDT cannot reconstitute the element from its handle identifier
  • Accept inaccurate search matches in ReferencesHandler when the search target itself is a non-Java element (contributed participants may not provide full accuracy information)
  • Resolve correct LSP language ID for hover MarkedStrings via SearchParticipantRegistry instead of hardcoding "java"
  • Guard NavigateToDefinitionHandler.computeBreakContinue() with isJavaLikeFileName to prevent ClassCastException when ECJ's ASTParser attempts to parse a contributed ICompilationUnit (e.g., Kotlin)
  • Fix NavigateToDeclarationHandler to fall back to definition for non-METHOD elements (TYPE, FIELD) from non-Java files instead of returning null — enables textDocument/declaration for contributed languages
  • Fix NavigateToTypeDefinitionHandler to fall back to definition when resolveElementType() returns null for non-Java files — enables textDocument/typeDefinition for contributed languages whose declaring types have no Java counterpart
  • Log unhandled exceptions in BaseJDTLanguageServer.computeAsync() and JDTLanguageServer.computeAsyncWithClientProgress() instead of silently swallowing them
  • Extract SearchUtils.getContributedSearchParticipants() to centralize the "skip default participant at index 0" logic, fixing a bug where identity comparison against getDefaultSearchParticipant() never filtered anything (it creates a new instance per call)
  • Extract ContributedSearchRequestor in WorkspaceSymbolHandler to deduplicate two near-identical inline anonymous SearchRequestor implementations

Motivation

SearchEngine.search() already supports multiple SearchParticipant instances, but all call sites in jdtls hardcode a single-element array containing only the default Java participant. This prevents non-Java languages from contributing search results for cross-language features.

With the new org.eclipse.jdt.core.searchParticipant extension point (eclipse-jdt/eclipse.jdt.core#4938), languages like Kotlin can register search participants that index .kt files and contribute to JDT's search infrastructure. This PR wires jdtls to use those contributed participants.

Changes

1. Search call sites → getSearchParticipants()

ReferencesHandler, CodeLensHandler, ImplementationCollector, and HoverInfoProvider.isResolved() now call SearchEngine.getSearchParticipants() which returns [default, ...contributed].

2. WorkspaceSymbolHandler — contributed participant supplementation

searchAllTypeNames() and searchAllMethodNames() only query indexes through the default Java search participant (see BasicSearchEngine line 1987: getDefaultSearchParticipant(), // Java search only). Non-Java types and methods indexed by contributed participants are invisible to these APIs.

After both the existing searchAllTypeNames and searchAllMethodNames calls, supplementary search() calls are now run through contributed participants only (excluding the default). They create TYPE/METHOD DECLARATIONS patterns with the same match rule (camelcase or wildcard) used by the workspace symbol query, and convert SearchMatch results to SymbolInformation with proper location, container name, and symbol kind. Both searches share a single ContributedSearchRequestor class that handles both IType and IMethod elements via IMember.

3. ImplementationCollector — contributed participant supplementation

findTypeImplementations() uses type.newTypeHierarchy().getAllSubtypes() which only discovers Java subtypes. For non-Java types (e.g., a Kotlin class implementing a Java/Kotlin interface), the type hierarchy is empty. After the standard hierarchy lookup, a supplementary IMPLEMENTORS search is now run through contributed participants (via SearchUtils.getContributedSearchParticipants()) to discover non-Java implementations. Results are deduplicated by FQN against the types already found by the hierarchy.

4. SearchUtils.getContributedSearchParticipants()

Centralizes the logic of obtaining contributed (non-default) search participants. SearchEngine.getSearchParticipants() returns the default Java participant at index 0 followed by contributed participants; this utility returns only the contributed portion. Previously, call sites used identity comparison against getDefaultSearchParticipant() which never worked correctly — getDefaultSearchParticipant() creates a new JavaSearchParticipant instance per call, so != was always true and the default participant was never filtered out, causing duplicate results.

5. ReferencesHandler — accept inaccurate matches for non-Java elements

When the search target is a non-Java element (resolved via a contributed participant), ReferencesHandler now accepts inaccurate search matches. Contributed participants may not provide full accuracy information, and filtering these out would silently drop valid cross-language references.

6. resolveCompilationUnit(IFile) fallback

When a non-Java file (e.g., .kt) is opened, the method now queries contributed participants via getCompilationUnit(IFile) as a fallback after the standard Java resolution path. This enables document symbols, hover, go-to-definition, call hierarchy, and code lenses for derived source languages.

7. findElementsAtSelection() search participant fallback

When Java's codeSelect() returns empty (which happens for types and methods provided by non-Java languages), a new resolveViaSearchParticipants() helper extracts the identifier at the cursor and searches contributed participants for matching TYPE or METHOD declarations. This fixes Java→Kotlin navigation gaps (go-to-definition and hover on Kotlin facade classes and property accessors from Java files).

8. Type hierarchy support for contributed participants

  • Fallback element resolution: When TypeHierarchyHandler cannot reconstitute a IJavaElement from its handle identifier (which happens for contributed elements), it falls back to codeSelect() at the element's source position to re-resolve it
  • Contributed supertype resolution (resolveContributedSupertypes): For non-Java types, JDT's newSupertypeHierarchy() does not work. Instead, the superclass and superinterface names are resolved via SearchEngine search across all participants (hoisted to avoid N+1 array allocations), building the hierarchy items from the resolved IType instances
  • Contributed subtype supplementation (supplementWithContributedSubtypes): After JDT computes subtypes via its own hierarchy, a secondary IMPLEMENTORS search is run against contributed participants only (via SearchUtils.getContributedSearchParticipants()) to discover additional subtypes from non-Java languages

9. Correct language ID for hover MarkedStrings

HoverInfoProvider.getLanguageId() queries SearchParticipantRegistry for the file extension and its associated LSP language ID, falling back to "java" for standard Java elements. This ensures hover popups render syntax highlighting for the correct language.

10. NavigateToDefinitionHandler — guard ECJ AST parsing for non-Java files

computeBreakContinue() uses ASTParser.createAST() to parse break/continue statements, which internally casts the ICompilationUnit to ECJ's org.eclipse.jdt.internal.compiler.env.ICompilationUnit. Contributed compilation units (e.g., KotlinCompilationUnit) do not implement this internal interface, causing a ClassCastException. Added JavaCore.isJavaLikeFileName() guard to skip break/continue resolution for non-Java files.

11. NavigateToDeclarationHandler — fallback for non-Java files

textDocument/declaration only made sense for METHOD elements (finding the declaring method in the type hierarchy). For non-Java files, TYPE and FIELD elements were rejected before reaching the non-Java fallback. The check is now split: non-METHOD elements from non-Java files fall back to computeDefinitionNavigation(), while Java files preserve existing behavior (return null for non-METHOD elements).

12. NavigateToTypeDefinitionHandler — fallback for non-Java files

textDocument/typeDefinition uses resolveElementType() which calls IType.resolveType(typeName) on the declaring type. For contributed types whose declaring type is a pure non-Java type with no Java counterpart, resolveType() returns null. Added fallback: when type resolution fails and the file is non-Java, delegate to computeDefinitionNavigation(element) to navigate to the element itself rather than returning nothing.

13. Log unhandled exceptions in computeAsync()

BaseJDTLanguageServer.computeAsync() and JDTLanguageServer.computeAsyncWithClientProgress() now attach a whenComplete() handler that logs unhandled exceptions instead of silently swallowing them. This makes failures in LSP request handlers visible in the server log for diagnosis.

Backward Compatibility

When no search participant extensions are registered, getSearchParticipants() returns only the default Java participant, and the fallback paths only run when standard JDT resolution already returned empty. Behavior is identical to before.

Test plan

SearchParticipantsTest (14 tests), WorkspaceSymbolHandlerTest (17 tests), and TypeHierarchyHandlerTest (4 tests) all pass. Tests verify:

  • getSearchParticipants() API consistency (includes default, correct class, stable across calls)
  • Default participant's getCompilationUnit() returns null
  • resolveCompilationUnit fallback loop runs without error for non-Java files (returns null when no contributed participant is registered)
  • resolveCompilationUnit still works for Java files
  • Workspace symbol search produces no duplicate results (wildcard and exact match)
  • Hover MarkedString language ID is "java" for Java elements
  • findElementsAtSelection resolves Java elements and returns empty for whitespace
  • Type hierarchy produces no duplicate subtypes and supertypes still resolve
  • Definition navigation works for Java files (guard does not block computeBreakContinue)
  • All pre-existing WorkspaceSymbolHandlerTest and TypeHierarchyHandlerTest tests pass (no regressions)
  • Declaration and type definition handler fallbacks tested via karellen-jdtls-kotlin integration tests (371 tests)

SearchEngine.search() already supports multiple SearchParticipant instances,
but all call sites in jdtls hardcode a single-element array containing only
the default Java participant. This prevents non-Java languages from
contributing index entries for cross-language call hierarchy, find references,
type hierarchy, workspace symbol, and implementation search.

This change updates all four search call sites (ReferencesHandler,
CodeLensHandler, ImplementationCollector, HoverInfoProvider) to use the new
JDT Core API SearchEngine.getSearchParticipants(), which returns the default
participant plus any participants contributed via the
org.eclipse.jdt.core.derivedSourceSearchParticipant extension point.

WorkspaceSymbolHandler: supplement searchAllTypeNames() results with a
search() call through contributed participants. searchAllTypeNames() only
queries the default (Java) participant's indexes, making non-Java types
invisible to workspace/symbol queries. The supplementary search creates a
TYPE DECLARATIONS pattern with the same match rule (camelcase or wildcard)
and runs it through contributed participants only, converting SearchMatch
results to SymbolInformation with proper location, container, and kind.

ReferencesHandler: accept A_INACCURATE matches for contributed (non-Java)
elements, since JDT's MatchLocator cannot resolve type bindings for types
not compiled by ECJ.

TypeHierarchyHandler: supplement JDT's native type hierarchy with contributed
search participants. For subtypes, run a supplementary IMPLEMENTORS search
via contributed participants to discover non-Java types that extend or
implement the target type. For supertypes of contributed types, resolve
declared supertype names via type declaration search since JDT's
newSupertypeHierarchy() does not work for non-Java IType implementations.
Re-resolve contributed elements via codeSelect when JavaCore.create() cannot
reconstitute them from handle identifiers.

ImplementationCollector.findTypeImplementations(): supplement JDT's
newTypeHierarchy().getAllSubtypes() with an IMPLEMENTORS search through
contributed participants to discover non-Java types that implement the target
interface or extend the target class.

HoverInfoProvider: use DerivedSourceSearchParticipantRegistry.getLanguageId()
to tag hover MarkedString content with the correct LSP language identifier
(e.g. "kotlin" instead of "java") for elements from contributed search
participants.

JDTUtils.resolveCompilationUnit(IFile) now falls back to querying contributed
DerivedSourceSearchParticipant.getCompilationUnit(IFile) for non-Java source
files, enabling document symbols, hover, go-to-definition, call hierarchy,
and code lenses for derived source languages (e.g., Kotlin .kt files).

JDTUtils.findElementsAtSelection() now falls back to searching contributed
participants when Java's codeSelect() returns empty. This enables
go-to-definition and hover for types and methods provided by non-Java
languages (e.g., Kotlin facade classes and property accessors) from Java
source files.

NavigateToDefinitionHandler.computeBreakContinue() now guarded with
isJavaLikeFileName check to prevent ClassCastException when ASTParser
attempts to cast a contributed ICompilationUnit (e.g. KotlinCompilationUnit)
to ECJ's internal ICompilationUnit interface.

NavigateToDeclarationHandler: split the non-METHOD element check so that
non-Java files (e.g. .kt) fall back to computeDefinitionNavigation() for
TYPE and FIELD elements instead of returning null. Java files preserve
existing behavior (only METHOD elements proceed to declaration search).

NavigateToTypeDefinitionHandler: add fallback for non-Java files when
resolveElementType() returns null. For contributed types where the declaring
type has no Java counterpart, the type signature cannot be resolved via
IType.resolveType(). The fallback delegates to
computeDefinitionNavigation(element) to navigate to the element itself.

BaseJDTLanguageServer.computeAsync() and JDTLanguageServer
.computeAsyncWithClientProgress() now log unhandled exceptions via
whenComplete() instead of silently swallowing them, making failures in LSP
request handlers visible in the server log.

When no extensions are registered, behavior is identical to before (only the
default Java participant is used), ensuring zero regression risk.

SearchUtils.getContributedSearchParticipants() centralizes the "skip default
participant at index 0" logic used by JDTUtils, WorkspaceSymbolHandler,
TypeHierarchyHandler, and ImplementationCollector. The previous identity
comparison against getDefaultSearchParticipant() never filtered anything
since it creates a new instance per call.

WorkspaceSymbolHandler: extract ContributedSearchRequestor to deduplicate
two near-identical inline anonymous SearchRequestors for type and method
contributed search.

TypeHierarchyHandler: hoist getSearchParticipants() call in
resolveContributedSupertypes to avoid N+1 array allocations.

DerivedSourceSearchParticipantsTest expanded to 14 tests covering workspace symbol
deduplication, hover language ID, findElementsAtSelection fallback, type
hierarchy deduplication, and definition navigation.
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.

1 participant