From de9d34b3955b3f453a43eb5ca69508b573c356ad Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Sun, 8 Mar 2026 18:28:26 -0400 Subject: [PATCH] Use SearchEngine.getSearchParticipants() for non-Java language search 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.searchParticipant 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. HoverInfoProvider: use SearchParticipantRegistry.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 SearchParticipant.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. 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. --- .../core/internal/BaseJDTLanguageServer.java | 9 +- .../ls/core/internal/HoverInfoProvider.java | 39 ++- .../jdt/ls/core/internal/JDTUtils.java | 98 +++++++ .../internal/handlers/CodeLensHandler.java | 3 +- .../handlers/ImplementationCollector.java | 2 +- .../internal/handlers/JDTLanguageServer.java | 6 + .../handlers/NavigateToDefinitionHandler.java | 2 +- .../internal/handlers/ReferencesHandler.java | 13 +- .../handlers/TypeHierarchyHandler.java | 273 ++++++++++++++++-- .../handlers/WorkspaceSymbolHandler.java | 95 ++++++ .../handlers/SearchParticipantsTest.java | 96 ++++++ 11 files changed, 603 insertions(+), 33 deletions(-) create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SearchParticipantsTest.java diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java index b83b5595d9..c6854ac8cc 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java @@ -84,7 +84,14 @@ protected void toggleCapability(boolean enabled, String id, String capability, O } protected CompletableFuture computeAsync(Function code) { - return CompletableFutures.computeAsync(cc -> code.apply(toMonitor(cc))); + return CompletableFutures.computeAsync(cc -> code.apply(toMonitor(cc))) + .whenComplete((result, error) -> { + if (error != null) { + JavaLanguageServerPlugin.logException( + "Unhandled exception in LSP request handler", + error); + } + }); } protected IProgressMonitor toMonitor(CancelChecker checker) { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java index e8a5247e86..a8b52a50b3 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java @@ -43,12 +43,12 @@ import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; -import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.core.BinaryMember; import org.eclipse.jdt.internal.core.JrtPackageFragmentRoot; import org.eclipse.jdt.internal.core.manipulation.JavaElementLabelsCore; +import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.IVMInstall2; import org.eclipse.jdt.launching.JavaRuntime; @@ -81,7 +81,36 @@ public class HoverInfoProvider { private static final long COMMON_SIGNATURE_FLAGS = LABEL_FLAGS & ~JavaElementLabelsCore.ALL_FULLY_QUALIFIED | JavaElementLabelsCore.T_FULLY_QUALIFIED | JavaElementLabelsCore.M_FULLY_QUALIFIED; - private static final String LANGUAGE_ID = "java"; + private static final String DEFAULT_LANGUAGE_ID = "java"; + + /** + * Returns the LSP language identifier for the given element. + * Queries the SearchParticipantRegistry for contributed (non-Java) + * elements; falls back to "java" for standard Java elements. + */ + private static String getLanguageId(IJavaElement element) { + if (element != null) { + ICompilationUnit cu = (element instanceof IMember m) + ? m.getCompilationUnit() + : (element instanceof ILocalVariable lv) + ? (ICompilationUnit) lv.getAncestor( + IJavaElement.COMPILATION_UNIT) + : null; + if (cu != null) { + String fileName = cu.getElementName(); + String ext = SearchParticipantRegistry + .getFileExtension(fileName); + if (ext != null) { + String langId = SearchParticipantRegistry + .getLanguageId(ext); + if (langId != null) { + return langId; + } + } + } + } + return DEFAULT_LANGUAGE_ID; + } private final ITypeRoot unit; @@ -193,7 +222,7 @@ private boolean isResolved(IJavaElement element, IProgressMonitor monitor) throw SearchEngine engine = new SearchEngine(); IJavaSearchScope scope = SearchEngine.createJavaSearchScope(new IJavaElement[] { unit }, IJavaSearchScope.SOURCES | IJavaSearchScope.APPLICATION_LIBRARIES | IJavaSearchScope.SYSTEM_LIBRARIES); try { - engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, scope, new SearchRequestor() { + engine.search(pattern, SearchEngine.getSearchParticipants(), scope, new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { @@ -246,7 +275,7 @@ public static MarkedString computeSignature(IJavaElement element) { elementLabel = elementLabel + " = " + constantValue; } } - return new MarkedString(LANGUAGE_ID, elementLabel); + return new MarkedString(getLanguageId(element), elementLabel); } private static String getDefaultValue(IMethod method) { @@ -292,7 +321,7 @@ public static MarkedString computeJavadoc(IJavaElement element) throws CoreExcep } } } - return result != null ? new MarkedString(LANGUAGE_ID, result) : null; + return result != null ? new MarkedString(getLanguageId(element), result) : null; } public static String getSourceInfo(IJavaElement element) throws JavaModelException { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java index fc8eb6d439..4b090046e5 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java @@ -108,8 +108,13 @@ import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import org.eclipse.jdt.core.manipulation.CoreASTProvider; import org.eclipse.jdt.core.manipulation.SharedASTProviderCore; +import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.codeassist.InternalCompletionProposal; import org.eclipse.jdt.internal.codeassist.impl.Engine; import org.eclipse.jdt.internal.compiler.lookup.Binding; @@ -222,6 +227,15 @@ public static ICompilationUnit resolveCompilationUnit(IFile resource) { return JavaCore.createCompilationUnitFrom(resource); } } + // Fallback: ask contributed search participants for non-Java source files + // Skip index 0 (default JavaSearchParticipant) — it never provides a CU + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + for (int i = 1; i < participants.length; i++) { + ICompilationUnit cu = participants[i].getCompilationUnit(resource); + if (cu != null) { + return cu; + } + } } return null; @@ -1104,11 +1118,95 @@ public static IJavaElement[] findElementsAtSelection(ITypeRoot unit, int line, i } } } + // Fallback: ask contributed search participants for non-Java types. + // Java's codeSelect returns empty for types provided by non-Java + // languages (e.g., Kotlin facade classes, property accessors). + if ((elements == null || elements.length == 0) && unit.getJavaProject() != null) { + IJavaElement resolved = resolveViaSearchParticipants(unit, offset, monitor); + if (resolved != null) { + return new IJavaElement[] { resolved }; + } + } return elements; } return null; } + /** + * Extracts the Java identifier at the given offset from the buffer and + * searches contributed search participants for a matching TYPE or METHOD + * declaration. Returns the first match, or {@code null} if none found. + *

+ * This provides a fallback for types and methods provided by non-Java + * languages (e.g., Kotlin facade classes and property accessors) whose + * source is not visible to Java's {@code codeSelect()}. + */ + private static IJavaElement resolveViaSearchParticipants(ITypeRoot unit, int offset, IProgressMonitor monitor) { + try { + IBuffer buffer = unit.getBuffer(); + if (buffer == null) { + return null; + } + int length = buffer.getLength(); + int start = offset; + while (start > 0 && Character.isJavaIdentifierPart(buffer.getChar(start - 1))) { + start--; + } + int end = offset; + while (end < length && Character.isJavaIdentifierPart(buffer.getChar(end))) { + end++; + } + if (start == end) { + return null; + } + String identifier = buffer.getText(start, end - start); + + // Search contributed participants for TYPE declarations first, + // then METHOD declarations (for property accessors like getXxx) + int[] searchTypes = { IJavaSearchConstants.TYPE, IJavaSearchConstants.METHOD }; + IJavaSearchScope scope = SearchEngine.createJavaSearchScope( + new IJavaElement[] { unit.getJavaProject() }); + SearchEngine engine = new SearchEngine(); + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + for (int searchType : searchTypes) { + SearchPattern pattern = SearchPattern.createPattern( + identifier, searchType, IJavaSearchConstants.DECLARATIONS, + SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE); + if (pattern == null) { + continue; + } + IJavaElement[] result = new IJavaElement[1]; + try { + engine.search(pattern, participants, scope, + new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) + throws CoreException { + if (result[0] == null + && match.getAccuracy() != SearchMatch.A_INACCURATE + && match.getElement() instanceof IJavaElement el) { + result[0] = el; + throw new OperationCanceledException(); + } + } + }, monitor); + } catch (OperationCanceledException e) { + // first match found — stop searching + } catch (CoreException e) { + JavaLanguageServerPlugin.logException( + "Error resolving via search participants", e); + } + if (result[0] != null) { + return result[0]; + } + } + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException( + "Error extracting identifier for participant search", e); + } + return null; + } + public static boolean isSameParameters(IMethod method1, IMethod method2) { if (method1 == null || method2 == null) { return false; diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java index f8441471cb..44908e4965 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java @@ -37,7 +37,6 @@ import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; -import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.ls.core.internal.JDTUtils; @@ -148,7 +147,7 @@ private List findReferences(IJavaElement element, IProgressMonitor mon SearchPattern pattern = SearchPattern.createPattern(element, IJavaSearchConstants.REFERENCES); final List result = new ArrayList<>(); SearchEngine engine = new SearchEngine(); - engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, createSearchScope(), new SearchRequestor() { + engine.search(pattern, SearchEngine.getSearchParticipants(), createSearchScope(), new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ImplementationCollector.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ImplementationCollector.java index 979b27e0f2..5abd0c68c1 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ImplementationCollector.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ImplementationCollector.java @@ -217,7 +217,7 @@ public void acceptSearchMatch(SearchMatch match) throws CoreException { int limitTo = IJavaSearchConstants.DECLARATIONS | IJavaSearchConstants.IGNORE_DECLARING_TYPE | IJavaSearchConstants.IGNORE_RETURN_TYPE; SearchPattern pattern = SearchPattern.createPattern(method, limitTo); Assert.isNotNull(pattern); - SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }; + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); SearchEngine engine = new SearchEngine(); engine.search(pattern, participants, hierarchyScope, requestor, new SubProgressMonitor(monitor, 7)); if (monitor.isCanceled()) { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java index 80a883c6c7..ad8b9a29c4 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java @@ -1273,6 +1273,12 @@ private CompletableFuture computeAsyncWithClientProgress(Function { IProgressMonitor monitor = progressReporterManager.getProgressReporter(cc); return code.apply(monitor); + }).whenComplete((result, error) -> { + if (error != null) { + JavaLanguageServerPlugin.logException( + "Unhandled exception in LSP request handler", + error); + } }); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java index 411fbeffd4..e31f664c22 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java @@ -85,7 +85,7 @@ private Location computeDefinitionNavigation(ITypeRoot unit, int line, int colum if (monitor.isCanceled()) { return null; } - if (element == null) { + if (element == null && JavaCore.isJavaLikeFileName(unit.getElementName())) { return computeBreakContinue(unit, line, column); } return computeDefinitionNavigation(element, unit.getJavaProject()); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ReferencesHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ReferencesHandler.java index 47ead81786..97b857ebf4 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ReferencesHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ReferencesHandler.java @@ -39,7 +39,6 @@ import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; -import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.corext.codemanipulation.GetterSetterUtil; @@ -177,17 +176,25 @@ private boolean isInsideJRE(IJavaElement element) { public void search(IJavaElement elementToSearch, final List locations, IProgressMonitor monitor, boolean isIncludeDeclaration) throws CoreException, JavaModelException { boolean includeClassFiles = preferenceManager.isClientSupportsClassFileContent(); boolean includeDecompiledSources = preferenceManager.getPreferences().isIncludeDecompiledSources(); + // When the search target is a non-Java element (e.g., from + // a contributed SearchParticipant like Kotlin), the Java + // MatchLocator cannot fully resolve the declaring type + // binding and reports matches as A_INACCURATE. These + // matches are still valid — the method name and parameter + // count match — so accept them. + ICompilationUnit cu = (elementToSearch instanceof IMember m) ? m.getCompilationUnit() : null; + boolean acceptInaccurate = cu != null && !JavaCore.isJavaLikeFileName(cu.getElementName()); SearchEngine engine = new SearchEngine(); SearchPattern pattern = SearchPattern.createPattern(elementToSearch, IJavaSearchConstants.REFERENCES); if (isIncludeDeclaration) { SearchPattern patternDecl = SearchPattern.createPattern(elementToSearch, IJavaSearchConstants.DECLARATIONS); pattern = SearchPattern.createOrPattern(pattern, patternDecl); } - engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, createSearchScope(elementToSearch), new SearchRequestor() { + engine.search(pattern, SearchEngine.getSearchParticipants(), createSearchScope(elementToSearch), new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { - if (match.getAccuracy() == SearchMatch.A_INACCURATE) { + if (!acceptInaccurate && match.getAccuracy() == SearchMatch.A_INACCURATE) { return; } Object o = match.getElement(); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java index b767336dc9..212d36a8d7 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java @@ -16,12 +16,16 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IOrdinaryClassFile; @@ -31,6 +35,13 @@ import org.eclipse.jdt.core.ITypeRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; import org.eclipse.jdt.internal.core.JavaModelManager; import org.eclipse.jdt.ls.core.internal.JDTUtils; @@ -134,7 +145,33 @@ private List getTypeHierarchyItems(TypeHierarchyItem item, Ty member = (IMember) element; } else if (element instanceof IOrdinaryClassFile classFile) { member = classFile.getType(); - } else { + } + if (member == null) { + // Element could not be reconstituted via JavaCore.create() — + // this happens for contributed (non-Java) elements whose + // handles are not resolvable by the Java model. Re-resolve + // via codeSelect using the item's URI and position. + String uri = item.getUri(); + if (uri != null) { + try { + ITypeRoot typeRoot = JDTUtils.resolveTypeRoot(uri); + if (typeRoot != null) { + Position pos = item.getSelectionRange().getStart(); + IJavaElement resolved = JDTUtils.findElementAtSelection( + typeRoot, pos.getLine(), pos.getCharacter(), + JavaLanguageServerPlugin.getPreferencesManager(), + monitor); + if (resolved instanceof IType || resolved instanceof IMethod) { + member = (IMember) resolved; + } + } + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException( + "Failed to resolve type hierarchy element from " + uri, e); + } + } + } + if (member == null) { return Collections.emptyList(); } return resolveTypeHierarchyItems(member, targetMethod, direction, monitor); @@ -154,40 +191,236 @@ private List resolveTypeHierarchyItems(IMember member, IMetho ITypeHierarchy typeHierarchy = null; List items = new ArrayList<>(); IType[] hierarchyTypes = null; + boolean isContributedElement = false; + ICompilationUnit cu = type.getCompilationUnit(); + if (cu != null) { + isContributedElement = !JavaCore.isJavaLikeFileName( + cu.getElementName()); + } if (direction == TypeHierarchyDirection.Supertype) { - typeHierarchy = type.newSupertypeHierarchy(DefaultWorkingCopyOwner.PRIMARY, monitor); - hierarchyTypes = typeHierarchy.getSupertypes(type); + if (isContributedElement) { + // JDT's newSupertypeHierarchy() doesn't work for + // contributed (non-Java) types. Resolve supertypes + // from the element's declared supertype names. + hierarchyTypes = resolveContributedSupertypes( + type, monitor); + } else { + typeHierarchy = type.newSupertypeHierarchy( + DefaultWorkingCopyOwner.PRIMARY, monitor); + hierarchyTypes = typeHierarchy.getSupertypes(type); + } } else { - ICompilationUnit[] workingCopies = JavaModelManager.getJavaModelManager().getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, true); - typeHierarchy = type.newTypeHierarchy(workingCopies, monitor); - hierarchyTypes = typeHierarchy.getSubtypes(type); + if (!isContributedElement) { + ICompilationUnit[] workingCopies = JavaModelManager + .getJavaModelManager().getWorkingCopies( + DefaultWorkingCopyOwner.PRIMARY, + true); + typeHierarchy = type.newTypeHierarchy( + workingCopies, monitor); + hierarchyTypes = typeHierarchy.getSubtypes(type); + } else { + hierarchyTypes = new IType[0]; + } } + Set seen = new HashSet<>(); for (IType hierarchyType : hierarchyTypes) { if (monitor.isCanceled()) { return Collections.emptyList(); } - TypeHierarchyItem item = null; - if (targetMethod != null) { - IMethod[] matches = hierarchyType.findMethods(targetMethod); - boolean excludeMember = matches == null || matches.length == 0; - // Do not show java.lang.Object unless target method is based there - if (!excludeMember || !"java.lang.Object".equals(hierarchyType.getFullyQualifiedName())) { - item = TypeHierarchyHandler.toTypeHierarchyItem(excludeMember ? hierarchyType : matches[0], excludeMember, targetMethod); - } - } else { - item = TypeHierarchyHandler.toTypeHierarchyItem(hierarchyType); - } - if (item == null) { - continue; + TypeHierarchyItem item = toHierarchyItem( + hierarchyType, targetMethod); + if (item != null) { + items.add(item); + seen.add(hierarchyType.getFullyQualifiedName()); } - items.add(item); + } + // Supplement subtypes with contributed search participants + // to discover non-Java types (e.g., Kotlin) that extend + // or implement this type. + if (direction == TypeHierarchyDirection.Subtype) { + supplementWithContributedSubtypes( + type, items, seen, targetMethod, monitor); } return items; } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException( + "Failed to resolve type hierarchy", e); return Collections.emptyList(); } } + private TypeHierarchyItem toHierarchyItem(IType hierarchyType, + IMethod targetMethod) throws JavaModelException { + if (targetMethod != null) { + IMethod[] matches = hierarchyType.findMethods(targetMethod); + boolean excludeMember = matches == null + || matches.length == 0; + if (!excludeMember || !"java.lang.Object".equals( + hierarchyType.getFullyQualifiedName())) { + return TypeHierarchyHandler.toTypeHierarchyItem( + excludeMember ? hierarchyType : matches[0], + excludeMember, targetMethod); + } + return null; + } + return TypeHierarchyHandler.toTypeHierarchyItem(hierarchyType); + } + + /** + * Resolves supertypes for a contributed (non-Java) type element by + * reading its declared supertype names and resolving them. Simple + * names are resolved first via the Java model (as FQN), then via + * type declaration search across all search participants. + */ + private IType[] resolveContributedSupertypes(IType type, + IProgressMonitor monitor) throws JavaModelException { + IJavaProject javaProject = type.getJavaProject(); + List supertypes = new ArrayList<>(); + String superclassName = type.getSuperclassName(); + if (superclassName != null) { + IType resolved = resolveTypeName( + superclassName, javaProject, monitor); + if (resolved != null) { + supertypes.add(resolved); + } + } + String[] interfaceNames = type.getSuperInterfaceNames(); + for (String ifName : interfaceNames) { + IType resolved = resolveTypeName( + ifName, javaProject, monitor); + if (resolved != null) { + supertypes.add(resolved); + } + } + return supertypes.toArray(new IType[0]); + } + + /** + * Resolves a type name (simple or fully qualified) to an IType. + * Tries direct lookup first, then falls back to a type declaration + * search across all search participants. + */ + private IType resolveTypeName(String typeName, + IJavaProject javaProject, IProgressMonitor monitor) { + if (typeName == null) { + return null; + } + // Try direct FQN lookup first + if (javaProject != null) { + try { + IType type = javaProject.findType(typeName); + if (type != null && type.exists()) { + return type; + } + } catch (JavaModelException e) { + // Fall through to search + } + } + // Search for type declarations matching the simple name + try { + SearchPattern pattern = SearchPattern.createPattern( + typeName, IJavaSearchConstants.TYPE, + IJavaSearchConstants.DECLARATIONS, + SearchPattern.R_EXACT_MATCH + | SearchPattern.R_CASE_SENSITIVE); + if (pattern == null) { + return null; + } + IJavaSearchScope scope = javaProject != null + ? SearchEngine.createJavaSearchScope( + new IJavaElement[]{javaProject}) + : SearchEngine.createWorkspaceScope(); + IType[] result = new IType[1]; + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + if (result[0] == null + && match.getElement() instanceof IType t + && t.exists()) { + result[0] = t; + } + } + }; + new SearchEngine().search(pattern, + SearchEngine.getSearchParticipants(), + scope, requestor, monitor); + return result[0]; + } catch (CoreException e) { + JavaLanguageServerPlugin.logException( + "Error resolving type name: " + typeName, e); + return null; + } + } + + /** + * Supplements the subtype list with types found via contributed + * search participants (e.g., Kotlin types that extend a Java type). + * Uses a SUPERTYPE_TYPE_REFERENCE search to find types that declare + * the given type as their supertype. + */ + private void supplementWithContributedSubtypes(IType type, + List items, Set seen, + IMethod targetMethod, IProgressMonitor monitor) { + try { + SearchPattern pattern = SearchPattern.createPattern( + type.getFullyQualifiedName(), + IJavaSearchConstants.TYPE, + IJavaSearchConstants.IMPLEMENTORS, + SearchPattern.R_EXACT_MATCH + | SearchPattern.R_CASE_SENSITIVE); + if (pattern == null) { + return; + } + SearchParticipant[] participants = + SearchEngine.getSearchParticipants(); + SearchParticipant defaultParticipant = + SearchEngine.getDefaultSearchParticipant(); + List contributed = new ArrayList<>(); + for (SearchParticipant p : participants) { + if (p != defaultParticipant) { + contributed.add(p); + } + } + if (contributed.isEmpty()) { + return; + } + IJavaSearchScope scope = + SearchEngine.createWorkspaceScope(); + List foundTypes = new ArrayList<>(); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + Object element = match.getElement(); + if (element instanceof IType t) { + foundTypes.add(t); + } + } + }; + new SearchEngine().search(pattern, + contributed.toArray(new SearchParticipant[0]), + scope, requestor, monitor); + for (IType foundType : foundTypes) { + if (monitor.isCanceled()) { + return; + } + String fqn = foundType.getFullyQualifiedName(); + if (seen.contains(fqn)) { + continue; + } + TypeHierarchyItem item = toHierarchyItem( + foundType, targetMethod); + if (item != null) { + items.add(item); + seen.add(fqn); + } + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException( + "Error supplementing type hierarchy with " + + "contributed participants", e); + } + } + private IMember getMember(String uri, Position position, IProgressMonitor monitor) throws JavaModelException { IJavaElement typeElement = findTypeElement(JDTUtils.resolveTypeRoot(uri), position, monitor); if (typeElement instanceof IType type) { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java index 4edb7c1375..66f43ae7a4 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java @@ -23,13 +23,17 @@ import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.MethodNameMatch; import org.eclipse.jdt.core.search.MethodNameMatchRequestor; import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.core.search.TypeNameMatch; import org.eclipse.jdt.core.search.TypeNameMatchRequestor; import org.eclipse.jdt.ls.core.internal.JDTUtils; @@ -113,6 +117,80 @@ public static List search(String query, int maxResults, Strin // search for qualifier = qualiferName.typeName, type = null engine.searchAllTypeNames(tQuery.toCharArray(), qualifierMatchRule, null, typeMatchRule, IJavaSearchConstants.TYPE, searchScope, typeRequestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor); + // searchAllTypeNames only queries the default (Java) participant. + // Supplement with contributed participants (e.g. Kotlin) via search(). + SearchParticipant[] allParticipants = SearchEngine.getSearchParticipants(); + SearchParticipant defaultParticipant = SearchEngine.getDefaultSearchParticipant(); + List contributed = new ArrayList<>(); + for (SearchParticipant p : allParticipants) { + if (p != defaultParticipant) { + contributed.add(p); + } + } + if (!contributed.isEmpty() && !monitor.isCanceled()) { + SearchPattern typePattern = SearchPattern.createPattern( + tQuery, + IJavaSearchConstants.TYPE, + IJavaSearchConstants.DECLARATIONS, + typeMatchRule); + if (typePattern != null) { + SearchRequestor contributedRequestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + if (maxResults > 0 && symbols.size() >= maxResults) { + monitor.setCanceled(true); + return; + } + Object element = match.getElement(); + if (!(element instanceof IType type)) { + return; + } + Location location = null; + try { + if (!type.isBinary()) { + location = JDTUtils.toLocation(type); + } else { + if (sourceOnly) { + return; + } + if (type instanceof IMember member) { + location = SearchUtils.searchOtherSources(member); + } + if (location == null) { + location = JDTUtils.toLocation(type.getClassFile()); + } + } + } catch (Exception e) { + JavaLanguageServerPlugin.logException("Unable to determine location for " + type.getElementName(), e); + return; + } + if (location != null && type.getElementName() != null && !type.getElementName().isEmpty()) { + SymbolInformation symbolInformation = new SymbolInformation(); + symbolInformation.setContainerName(type.getDeclaringType() != null ? type.getDeclaringType().getFullyQualifiedName() : type.getPackageFragment() != null ? type.getPackageFragment().getElementName() : ""); + symbolInformation.setName(type.getElementName()); + symbolInformation.setKind(mapKind(type)); + try { + if (Flags.isDeprecated(type.getFlags())) { + if (isSymbolTagSupported) { + symbolInformation.setTags(List.of(SymbolTag.Deprecated)); + } else { + symbolInformation.setDeprecated(true); + } + } + } catch (JavaModelException e) { + // ignore flags resolution failure + } + symbolInformation.setLocation(location); + symbols.add(symbolInformation); + } + } + }; + engine.search(typePattern, + contributed.toArray(new SearchParticipant[0]), + searchScope, contributedRequestor, monitor); + } + } + if (preferenceManager != null && preferenceManager.getPreferences().isIncludeSourceMethodDeclarations()) { monitor.beginTask("Searching methods...", 100); IJavaSearchScope nonSourceSearchScope = createSearchScope(projectName, true); @@ -150,6 +228,23 @@ private static IJavaSearchScope createSearchScope(String projectName, boolean so return SearchEngine.createJavaSearchScope(excludeTestCode, targetProjects, scope); } + private static SymbolKind mapKind(IType type) { + try { + if (type.isInterface()) { + return SymbolKind.Interface; + } + if (type.isAnnotation()) { + return SymbolKind.Property; + } + if (type.isEnum()) { + return SymbolKind.Enum; + } + } catch (JavaModelException e) { + // ignore + } + return SymbolKind.Class; + } + public static class SearchSymbolParams extends WorkspaceSymbolParams { public String projectName; public boolean sourceOnly; diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SearchParticipantsTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SearchParticipantsTest.java new file mode 100644 index 0000000000..bd7e056cb0 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/SearchParticipantsTest.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Arcadiy Ivanov - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.junit.jupiter.api.Test; + +/** + * Tests that {@link SearchEngine#getSearchParticipants()} provides the + * participants used by jdtls search call sites. + */ +public class SearchParticipantsTest extends AbstractProjectsManagerBasedTest { + + @Test + public void testGetSearchParticipantsIncludesDefault() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + assertNotNull(participants); + assertTrue(participants.length >= 1, "Should contain at least the default participant"); + // First participant should be the default Java search participant + assertNotNull(participants[0]); + } + + @Test + public void testGetSearchParticipantsDefaultIsJavaParticipant() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + SearchParticipant defaultParticipant = SearchEngine.getDefaultSearchParticipant(); + // Both should be the same type (JavaSearchParticipant) + assertEquals(defaultParticipant.getClass(), participants[0].getClass(), + "First participant should be the default Java search participant"); + } + + @Test + public void testGetSearchParticipantsConsistentResults() { + SearchParticipant[] first = SearchEngine.getSearchParticipants(); + SearchParticipant[] second = SearchEngine.getSearchParticipants(); + assertEquals(first.length, second.length, + "Consecutive calls should return same number of participants"); + } + + @Test + public void testDefaultParticipantGetCompilationUnitReturnsNull() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + // Use an existing Java file as IFile — the default participant should still + // return null from getCompilationUnit() since it does not implement the method + IFile javaFile = project.getFile("src/java/Foo.java"); + assertTrue(javaFile.exists(), "Test file should exist"); + SearchParticipant defaultParticipant = SearchEngine.getDefaultSearchParticipant(); + ICompilationUnit cu = defaultParticipant.getCompilationUnit(javaFile); + assertNull(cu, "Default search participant should return null from getCompilationUnit()"); + } + + @Test + public void testResolveCompilationUnitNonJavaFileReturnsNull() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + // A non-Java file in a Java project — exercises the participant fallback loop + IFile nonJavaFile = project.getFile("src/java/Foo.kt"); + // File doesn't exist on disk, but resolveCompilationUnit(IFile) handles + // non-existent files gracefully — the important thing is the fallback runs + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(nonJavaFile); + assertNull(cu, "Non-Java file with no contributing participant should resolve to null"); + } + + @Test + public void testResolveCompilationUnitJavaFileStillWorks() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + IFile javaFile = project.getFile("src/java/Foo.java"); + assertTrue(javaFile.exists(), "Test file should exist"); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(javaFile); + assertNotNull(cu, "Java file should still resolve via the normal path"); + } +}