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"); + } +}