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..425e3ab522 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.DerivedSourceSearchParticipantRegistry; 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 DerivedSourceSearchParticipantRegistry 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 = DerivedSourceSearchParticipantRegistry + .getFileExtension(fileName); + if (ext != null) { + String langId = DerivedSourceSearchParticipantRegistry + .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..d3f1169ef4 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,14 @@ 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.DerivedSourceSearchParticipant; +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 +228,15 @@ public static ICompilationUnit resolveCompilationUnit(IFile resource) { return JavaCore.createCompilationUnitFrom(resource); } } + // Fallback: ask contributed search participants for non-Java source files + for (SearchParticipant p : SearchUtils.getContributedSearchParticipants()) { + if (p instanceof DerivedSourceSearchParticipant dsp) { + ICompilationUnit cu = dsp.getCompilationUnit(resource); + if (cu != null) { + return cu; + } + } + } } return null; @@ -1104,11 +1119,97 @@ 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) { + if (result[0] == null) { + throw e; + } + } 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/SearchUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SearchUtils.java index 5a3ca290ef..f0376cd85d 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SearchUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SearchUtils.java @@ -19,6 +19,8 @@ import java.io.InputStreamReader; import java.nio.file.Files; +import java.util.Arrays; + import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.IPath; @@ -30,6 +32,8 @@ import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; @@ -133,6 +137,20 @@ public void visitLineNumber(int line, Label start) { } } + private static final SearchParticipant[] EMPTY_PARTICIPANTS = new SearchParticipant[0]; + + /** + * Returns contributed search participants, excluding the default Java + * participant. {@link SearchEngine#getSearchParticipants()} places the + * default at index 0; this method returns the remainder. + */ + public static SearchParticipant[] getContributedSearchParticipants() { + SearchParticipant[] all = SearchEngine.getSearchParticipants(); + return all.length > 1 + ? Arrays.copyOfRange(all, 1, all.length) + : EMPTY_PARTICIPANTS; + } + public static Location searchOtherSources(IMember member) throws JavaModelException { PreferenceManager preferenceManager = JavaLanguageServerPlugin.getPreferencesManager(); if (member == null || member.getClassFile() == null || preferenceManager == null || !(preferenceManager.getPreferences().isAspectjSupportEnabled() || preferenceManager.getPreferences().isKotlinSupportEnabled() 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..e4af74fde7 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 @@ -16,8 +16,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.eclipse.core.runtime.Assert; @@ -56,6 +58,7 @@ import org.eclipse.jdt.internal.corext.util.MethodOverrideTester; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.Messages; +import org.eclipse.jdt.ls.core.internal.SearchUtils; import org.eclipse.jface.text.IRegion; @@ -136,12 +139,72 @@ private List findTypeImplementations(IProgressMonitor monitor) throws JavaMod if (monitor.isCanceled()) { throw new OperationCanceledException(); } + supplementWithContributedImplementations(type, results, allTypes, monitor); } finally { monitor.done(); } return results; } + /** + * Supplements the implementation list with types found via contributed + * search participants. JDT's type hierarchy only discovers Java subtypes; + * non-Java types (e.g., Kotlin) that implement or extend the target type + * are only discoverable via an IMPLEMENTORS search through contributed + * participants. + */ + private void supplementWithContributedImplementations(IType type, + List results, IType[] alreadyFound, + IProgressMonitor monitor) { + SearchParticipant[] contributed = + SearchUtils.getContributedSearchParticipants(); + if (contributed.length == 0) { + return; + } + try { + SearchPattern pattern = SearchPattern.createPattern( + type.getFullyQualifiedName(), + IJavaSearchConstants.TYPE, + IJavaSearchConstants.IMPLEMENTORS, + SearchPattern.R_EXACT_MATCH + | SearchPattern.R_CASE_SENSITIVE); + if (pattern == null) { + return; + } + Set seen = new HashSet<>(); + for (IType t : alreadyFound) { + seen.add(t.getFullyQualifiedName()); + } + List foundTypes = new ArrayList<>(); + new SearchEngine().search(pattern, contributed, + SearchEngine.createWorkspaceScope(), + new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + if (match.getElement() instanceof IType t) { + foundTypes.add(t); + } + } + }, monitor); + for (IType foundType : foundTypes) { + if (monitor.isCanceled()) { + return; + } + if (seen.contains(foundType.getFullyQualifiedName())) { + continue; + } + T result = mapper.convert(foundType, 0, 0); + if (result != null) { + results.add(result); + seen.add(foundType.getFullyQualifiedName()); + } + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException( + "Error searching contributed participants for implementations", e); + } + } + private List findMethodImplementations(IProgressMonitor monitor) throws CoreException { IMethod method = (IMethod) javaElement; try { @@ -217,7 +280,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/NavigateToDeclarationHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDeclarationHandler.java index 1abd552ef3..df5e9e6879 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDeclarationHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDeclarationHandler.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.ITypeRoot; @@ -64,7 +65,13 @@ public List declaration(TextDocumentPositionParams position, private Location computeDeclarationNavigation(ITypeRoot unit, int line, int column, IProgressMonitor monitor) { try { IJavaElement element = JDTUtils.findElementAtSelection(unit, line, column, this.preferenceManager, monitor); - if (monitor.isCanceled() || element == null || element.getElementType() != IJavaElement.METHOD) { + if (monitor.isCanceled() || element == null) { + return null; + } + if (element.getElementType() != IJavaElement.METHOD) { + if (!JavaCore.isJavaLikeFileName(unit.getElementName())) { + return NavigateToDefinitionHandler.computeDefinitionNavigation(element, unit.getJavaProject()); + } return null; } @@ -74,6 +81,9 @@ private Location computeDeclarationNavigation(ITypeRoot unit, int line, int colu IMethod methodDeclaration = tester.findDeclaringMethod(method, false); if (methodDeclaration == null) { + if (!JavaCore.isJavaLikeFileName(unit.getElementName())) { + return NavigateToDefinitionHandler.computeDefinitionNavigation(element, unit.getJavaProject()); + } return null; } 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/NavigateToTypeDefinitionHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToTypeDefinitionHandler.java index 6e9cd6da8d..bbe42509f3 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToTypeDefinitionHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToTypeDefinitionHandler.java @@ -22,10 +22,17 @@ import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.ILocalVariable; +import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.ITypeRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IBinding; @@ -66,9 +73,12 @@ private Location computeTypeDefinitionNavigation(ITypeRoot unit, int line, int c try { CompilationUnit ast = CoreASTProvider.getInstance().getAST(unit, CoreASTProvider.WAIT_YES, monitor); int offset = JsonRpcHelpers.toOffset(unit.getBuffer(), line, column); - if (ast == null || offset < 0) { + if (offset < 0) { return null; } + if (ast == null) { + return computeTypeDefinitionWithoutAST(unit, line, column, monitor); + } NodeFinder finder = new NodeFinder(ast, offset, 0); ASTNode coveringNode = finder.getCoveringNode(); if (coveringNode instanceof SimpleName name) { @@ -117,4 +127,62 @@ private Location computeTypeDefinitionNavigation(ITypeRoot unit, int line, int c } return null; } + + /** + * Fallback for non-Java compilation units where CoreASTProvider cannot + * produce a Java AST. Uses codeSelect to resolve the element, then + * navigates to its type definition. + */ + private Location computeTypeDefinitionWithoutAST(ITypeRoot unit, int line, int column, IProgressMonitor monitor) { + try { + PreferenceManager preferenceManager = JavaLanguageServerPlugin.getPreferencesManager(); + IJavaElement element = JDTUtils.findElementAtSelection(unit, line, column, preferenceManager, monitor); + if (element == null || monitor.isCanceled()) { + return null; + } + IType targetType = resolveElementType(element, unit.getJavaProject()); + if (targetType != null) { + return NavigateToDefinitionHandler.computeDefinitionNavigation(targetType, unit.getJavaProject()); + } + if (!JavaCore.isJavaLikeFileName(unit.getElementName())) { + return NavigateToDefinitionHandler.computeDefinitionNavigation(element, unit.getJavaProject()); + } + } catch (CoreException | IllegalArgumentException e) { + JavaLanguageServerPlugin.logException("Problem computing typeDefinition for " + unit.getElementName(), e); + } + return null; + } + + private static IType resolveElementType(IJavaElement element, IJavaProject project) throws JavaModelException { + if (element instanceof IType type) { + return type; + } + String typeSignature = null; + IType declaringType = null; + if (element instanceof IMethod method) { + typeSignature = method.getReturnType(); + declaringType = method.getDeclaringType(); + } else if (element instanceof IField field) { + typeSignature = field.getTypeSignature(); + declaringType = field.getDeclaringType(); + } else if (element instanceof ILocalVariable variable) { + typeSignature = variable.getTypeSignature(); + IJavaElement parent = variable.getParent(); + if (parent instanceof IMember member) { + declaringType = member.getDeclaringType(); + } + } + if (typeSignature == null) { + return null; + } + String typeName = Signature.toString(typeSignature); + if (declaringType != null) { + String[][] resolved = declaringType.resolveType(typeName); + if (resolved != null && resolved.length > 0) { + String fqn = resolved[0][0].isEmpty() ? resolved[0][1] : resolved[0][0] + "." + resolved[0][1]; + return project.findType(fqn); + } + } + return project.findType(typeName); + } } 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..dc2b485041 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,10 +35,18 @@ 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; import org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType; +import org.eclipse.jdt.ls.core.internal.SearchUtils; import org.eclipse.jdt.ls.core.internal.JSONUtility; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.lsp4j.Location; @@ -134,7 +146,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 +192,232 @@ 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); + TypeHierarchyItem item = toHierarchyItem( + hierarchyType, targetMethod); + if (item != null) { + items.add(item); + seen.add(hierarchyType.getFullyQualifiedName()); } - if (item == null) { - continue; - } - 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(); + SearchParticipant[] participants = + SearchEngine.getSearchParticipants(); + List supertypes = new ArrayList<>(); + String superclassName = type.getSuperclassName(); + if (superclassName != null) { + IType resolved = resolveTypeName( + superclassName, javaProject, + participants, monitor); + if (resolved != null) { + supertypes.add(resolved); + } + } + String[] interfaceNames = type.getSuperInterfaceNames(); + for (String ifName : interfaceNames) { + IType resolved = resolveTypeName( + ifName, javaProject, + participants, 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, + SearchParticipant[] participants, + 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, + participants, 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[] contributed = + SearchUtils.getContributedSearchParticipants(); + if (contributed.length == 0) { + 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, 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..7a8fbcf2f4 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,11 +117,50 @@ 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/searchAllMethodNames only query the default + // (Java) participant. Supplement with contributed participants. + SearchParticipant[] contributed = SearchUtils.getContributedSearchParticipants(); + if (contributed.length > 0 && !monitor.isCanceled()) { + SearchPattern typePattern = SearchPattern.createPattern( + tQuery, + IJavaSearchConstants.TYPE, + IJavaSearchConstants.DECLARATIONS, + typeMatchRule); + if (typePattern != null) { + try { + engine.search(typePattern, contributed, searchScope, + new ContributedSearchRequestor(symbols, maxResults, sourceOnly, isSymbolTagSupported, monitor), + monitor); + } catch (OperationCanceledException e) { + // max results reached — continue to method search + } + } + } + if (preferenceManager != null && preferenceManager.getPreferences().isIncludeSourceMethodDeclarations()) { monitor.beginTask("Searching methods...", 100); IJavaSearchScope nonSourceSearchScope = createSearchScope(projectName, true); WorkspaceSymbolMethodRequestor methodRequestor = new WorkspaceSymbolMethodRequestor(symbols, maxResults, isSymbolTagSupported, monitor); engine.searchAllMethodNames(null, SearchPattern.R_PATTERN_MATCH, query.trim().toCharArray(), typeMatchRule, nonSourceSearchScope, methodRequestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor); + + // searchAllMethodNames only queries the default (Java) participant. + // Supplement with contributed participants for non-Java method declarations. + if (contributed.length > 0 && !monitor.isCanceled()) { + SearchPattern methodPattern = SearchPattern.createPattern( + tQuery, + IJavaSearchConstants.METHOD, + IJavaSearchConstants.DECLARATIONS, + typeMatchRule); + if (methodPattern != null) { + try { + engine.search(methodPattern, contributed, nonSourceSearchScope, + new ContributedSearchRequestor(symbols, maxResults, sourceOnly, isSymbolTagSupported, monitor), + monitor); + } catch (OperationCanceledException e) { + // max results reached + } + } + } } } catch (Exception e) { if (e instanceof OperationCanceledException) { @@ -150,6 +193,96 @@ 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; + } + + private static class ContributedSearchRequestor extends SearchRequestor { + private final Set symbols; + private final int maxResults; + private final boolean sourceOnly; + private final boolean isSymbolTagSupported; + private final IProgressMonitor monitor; + + ContributedSearchRequestor(Set symbols, int maxResults, boolean sourceOnly, boolean isSymbolTagSupported, IProgressMonitor monitor) { + this.symbols = symbols; + this.maxResults = maxResults; + this.sourceOnly = sourceOnly; + this.isSymbolTagSupported = isSymbolTagSupported; + this.monitor = monitor; + } + + @Override + public void acceptSearchMatch(SearchMatch match) { + if (maxResults > 0 && symbols.size() >= maxResults) { + throw new OperationCanceledException(); + } + Object element = match.getElement(); + if (!(element instanceof IMember member)) { + return; + } + Location location = null; + try { + if (member instanceof IType type) { + if (!type.isBinary()) { + location = JDTUtils.toLocation(type); + } else { + if (sourceOnly) { + return; + } + location = SearchUtils.searchOtherSources(member); + if (location == null) { + location = JDTUtils.toLocation(type.getClassFile()); + } + } + } else { + location = JDTUtils.toLocation(member); + } + } catch (Exception e) { + JavaLanguageServerPlugin.logException("Unable to determine location for " + member.getElementName(), e); + return; + } + if (location == null || member.getElementName() == null || member.getElementName().isEmpty()) { + return; + } + SymbolInformation symbolInformation = new SymbolInformation(); + if (member instanceof IType type) { + symbolInformation.setContainerName(type.getDeclaringType() != null ? type.getDeclaringType().getFullyQualifiedName() : type.getPackageFragment() != null ? type.getPackageFragment().getElementName() : ""); + symbolInformation.setKind(mapKind(type)); + } else { + symbolInformation.setContainerName(member.getDeclaringType() != null ? member.getDeclaringType().getFullyQualifiedName() : ""); + symbolInformation.setKind(SymbolKind.Method); + } + symbolInformation.setName(member.getElementName()); + try { + if (Flags.isDeprecated(member.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); + } + } + public static class SearchSymbolParams extends WorkspaceSymbolParams { public String projectName; public boolean sourceOnly; diff --git a/org.eclipse.jdt.ls.tests/plugin.xml b/org.eclipse.jdt.ls.tests/plugin.xml index 5ba70a1f00..4df7401d54 100644 --- a/org.eclipse.jdt.ls.tests/plugin.xml +++ b/org.eclipse.jdt.ls.tests/plugin.xml @@ -48,4 +48,18 @@ order ="300" class = "org.eclipse.jdt.ls.core.internal.managers.NoopImporter"/> + + + + + + diff --git a/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/LangxType.langx b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/LangxType.langx new file mode 100644 index 0000000000..836b1f2f58 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/LangxType.langx @@ -0,0 +1,4 @@ +package java; +public class LangxType { + public void langxMethod() {} +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DerivedSourceSearchParticipantsTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DerivedSourceSearchParticipantsTest.java new file mode 100644 index 0000000000..fe495b9f7f --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DerivedSourceSearchParticipantsTest.java @@ -0,0 +1,345 @@ +/******************************************************************************* + * 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.search.DerivedSourceSearchParticipant; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.ls.core.internal.HoverInfoProvider; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.ResourceUtils; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.ReferenceContext; +import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for search participant integration in jdtls. + * Validates both the infrastructure (API consistency) and behavioral + * paths (no duplicates, correct language ID, fallback behavior). + */ +public class DerivedSourceSearchParticipantsTest extends AbstractProjectsManagerBasedTest { + + @BeforeEach + public void resetParticipantCounters() { + TestDerivedSourceSearchParticipant.reset(); + } + + // --- Infrastructure tests --- + + @Test + public void testGetSearchParticipantsIncludesDefault() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + assertNotNull(participants); + assertTrue(participants.length >= 1, "Should contain at least the default participant"); + assertNotNull(participants[0]); + } + + @Test + public void testGetSearchParticipantsDefaultIsJavaParticipant() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + SearchParticipant defaultParticipant = SearchEngine.getDefaultSearchParticipant(); + 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"); + for (int i = 0; i < first.length; i++) { + assertEquals(first[i].getClass(), second[i].getClass(), + "Participant class at index " + i + " should be consistent"); + } + } + + @Test + public void testDefaultParticipantIsNotDerivedSourceSearchParticipant() throws Exception { + SearchParticipant defaultParticipant = SearchEngine.getDefaultSearchParticipant(); + assertNotNull(defaultParticipant); + assertTrue(!(defaultParticipant instanceof DerivedSourceSearchParticipant), + "Default search participant should not be a DerivedSourceSearchParticipant"); + } + + @Test + public void testResolveCompilationUnitNonJavaFileReturnsNull() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + IFile nonJavaFile = project.getFile("src/java/Foo.kt"); + 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"); + } + + // --- Behavioral: workspace symbol search produces no duplicates --- + + @Test + public void testWorkspaceSymbolSearchNoDuplicatesWithContributedParticipants() throws Exception { + importProjects("eclipse/hello"); + List results = WorkspaceSymbolHandler.search("*", monitor); + Set deduped = new HashSet<>(results); + assertEquals(results.size(), deduped.size(), + "Workspace symbol search should not produce duplicate entries"); + } + + @Test + public void testWorkspaceSymbolSearchExactMatchNoDuplicates() throws Exception { + importProjects("eclipse/hello"); + List results = WorkspaceSymbolHandler.search("Foo", monitor); + Set deduped = new HashSet<>(results); + assertEquals(results.size(), deduped.size(), + "Exact match workspace symbol search should not produce duplicates"); + } + + // --- Behavioral: hover language ID for Java elements --- + + @Test + public void testHoverLanguageIdIsJavaForJavaElements() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + String uri = project.getFile("src/java/Foo.java").getLocationURI().toString(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + assertNotNull(cu); + IType type = cu.findPrimaryType(); + assertNotNull(type, "Should find primary type Foo"); + MarkedString signature = HoverInfoProvider.computeSignature(type); + assertNotNull(signature); + assertEquals("java", signature.getLanguage(), + "Java element hover should have language ID 'java'"); + } + + // --- Behavioral: findElementsAtSelection fallback --- + + @Test + public void testFindElementsAtSelectionReturnsJavaElement() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + String uri = project.getFile("src/java/Foo.java").getLocationURI().toString(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + assertNotNull(cu); + // Find the class declaration — "Foo" is on line 5 (0-indexed) + // codeSelect should resolve this directly without the fallback + IJavaElement[] elements = JDTUtils.findElementsAtSelection( + cu, 5, 13, preferenceManager, monitor); + assertNotNull(elements); + assertTrue(elements.length > 0, "Should find Foo class at selection"); + assertEquals("Foo", elements[0].getElementName()); + } + + @Test + public void testFindElementsAtSelectionOnWhitespaceReturnsEmpty() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + String uri = project.getFile("src/java/Foo.java").getLocationURI().toString(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + assertNotNull(cu); + // Line 0, column 0 is likely a comment or whitespace — should return + // null or empty, and the search participant fallback should also + // return null gracefully + IJavaElement[] elements = JDTUtils.findElementsAtSelection( + cu, 0, 0, preferenceManager, monitor); + assertTrue(elements == null || elements.length == 0, + "Should return empty for whitespace/comment position"); + } + + // --- Behavioral: type hierarchy with no contributed participants --- + + @Test + public void testTypeHierarchyNoDuplicateSubtypes() throws Exception { + importProjects("maven/salut"); + IProject project = WorkspaceHelper.getProject("salut"); + TypeHierarchyHandler handler = new TypeHierarchyHandler(); + TypeHierarchyPrepareParams params = new TypeHierarchyPrepareParams(); + String uriString = project.getFile("src/main/java/org/sample/CallHierarchy.java") + .getLocationURI().toString(); + params.setTextDocument(new TextDocumentIdentifier(uriString)); + params.setPosition(new Position(2, 43)); // Builder interface + List items = handler.prepareTypeHierarchy(params, monitor); + assertNotNull(items); + assertEquals(1, items.size()); + TypeHierarchySubtypesParams subtypesParams = new TypeHierarchySubtypesParams(); + subtypesParams.setItem(items.get(0)); + List subtypes = handler.getSubtypeItems(subtypesParams, monitor); + assertNotNull(subtypes); + // Verify no duplicates from contributed participant supplementation + Set names = new HashSet<>(); + for (TypeHierarchyItem subtype : subtypes) { + assertTrue(names.add(subtype.getName()), + "Duplicate subtype: " + subtype.getName()); + } + } + + @Test + public void testTypeHierarchySupertypesStillWork() throws Exception { + importProjects("maven/salut"); + IProject project = WorkspaceHelper.getProject("salut"); + TypeHierarchyHandler handler = new TypeHierarchyHandler(); + TypeHierarchyPrepareParams params = new TypeHierarchyPrepareParams(); + String uriString = project.getFile("src/main/java/org/sample/CallHierarchy.java") + .getLocationURI().toString(); + params.setTextDocument(new TextDocumentIdentifier(uriString)); + params.setPosition(new Position(7, 27)); // FooBuilder class + List items = handler.prepareTypeHierarchy(params, monitor); + assertNotNull(items); + assertEquals(1, items.size()); + TypeHierarchySupertypesParams supertypesParams = new TypeHierarchySupertypesParams(); + supertypesParams.setItem(items.get(0)); + List supertypes = handler.getSupertypeItems(supertypesParams, monitor); + assertNotNull(supertypes); + assertEquals(2, supertypes.size()); + } + + // --- Behavioral: NavigateToDefinition guard --- + + @Test + public void testNavigateToDefinitionJavaFileStillWorks() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + String uri = project.getFile("src/java/Foo.java").getLocationURI().toString(); + NavigateToDefinitionHandler handler = new NavigateToDefinitionHandler(preferenceManager); + TextDocumentPositionParams posParams = new TextDocumentPositionParams(); + posParams.setTextDocument(new TextDocumentIdentifier(uri)); + posParams.setPosition(new Position(5, 13)); // "Foo" class name + List definitions = + handler.definition(posParams, monitor); + assertNotNull(definitions); + assertTrue(definitions.size() > 0, + "Definition navigation should resolve for Java files"); + } + + // --- Contributed Language X participant tests --- + + @Test + public void testContributedParticipantIsRegistered() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + assertTrue(participants.length >= 2, + "Should have at least 2 participants (default + Language X)"); + boolean found = false; + for (SearchParticipant p : participants) { + if (p instanceof TestDerivedSourceSearchParticipant) { + found = true; + break; + } + } + assertTrue(found, + "getSearchParticipants() should include TestDerivedSourceSearchParticipant"); + } + + @Test + public void testContributedParticipantIsDerivedSourceSearchParticipant() { + SearchParticipant[] participants = SearchEngine.getSearchParticipants(); + boolean found = false; + for (SearchParticipant p : participants) { + if (p instanceof DerivedSourceSearchParticipant && p instanceof TestDerivedSourceSearchParticipant) { + found = true; + break; + } + } + assertTrue(found, + "Contributed participant should be a DerivedSourceSearchParticipant"); + } + + @Test + public void testParticipantInvokedDuringReferenceSearch() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + PreferenceManager pm = mock(PreferenceManager.class); + when(pm.getPreferences()).thenReturn(preferences); + when(pm.isClientSupportsClassFileContent()).thenReturn(false); + ReferencesHandler handler = new ReferencesHandler(pm); + + URI uri = project.getFile("src/java/Foo2.java").getRawLocationURI(); + String fileURI = ResourceUtils.fixURI(uri); + ReferenceParams param = new ReferenceParams(); + param.setPosition(new Position(5, 16)); + param.setContext(new ReferenceContext(false)); + param.setTextDocument(new TextDocumentIdentifier(fileURI)); + + TestDerivedSourceSearchParticipant.reset(); + handler.findReferences(param, monitor); + + assertTrue(TestDerivedSourceSearchParticipant.beginSearchingCount.get() > 0, + "Contributed participant's beginSearching() should be called during reference search"); + assertTrue(TestDerivedSourceSearchParticipant.doneSearchingCount.get() > 0, + "Contributed participant's doneSearching() should be called during reference search"); + assertTrue(TestDerivedSourceSearchParticipant.selectIndexesCount.get() > 0, + "Contributed participant's selectIndexes() should be called during reference search"); + } + + @Test + public void testParticipantInvokedDuringImplementationSearch() throws Exception { + importProjects("eclipse/hello"); + IProject project = WorkspaceHelper.getProject("hello"); + String uri = project.getFile("src/java/IFoo.java").getLocationURI().toString(); + + TestDerivedSourceSearchParticipant.reset(); + + TextDocumentPositionParams posParams = new TextDocumentPositionParams(); + posParams.setTextDocument(new TextDocumentIdentifier(uri)); + posParams.setPosition(new Position(5, 18)); + List implementations = + new ImplementationsHandler(preferenceManager).findImplementations(posParams, monitor); + assertNotNull(implementations); + + assertTrue(TestDerivedSourceSearchParticipant.beginSearchingCount.get() > 0, + "Contributed participant's beginSearching() should be called during implementation search"); + } + + @Test + public void testLangxFileIndexedByContributedParticipant() throws Exception { + importProjects("eclipse/hello"); + assertTrue(TestDerivedSourceSearchParticipant.indexDocumentCount.get() > 0, + "Contributed participant's indexDocument() should be called for .langx files"); + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSearchDocument.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSearchDocument.java new file mode 100644 index 0000000000..837bf2ed02 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSearchDocument.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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 org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.SearchDocument; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.internal.core.util.Util; + +public class TestDerivedSearchDocument extends SearchDocument { + + private IFile file; + + public TestDerivedSearchDocument(String documentPath, SearchParticipant participant) { + super(documentPath, participant); + } + + @Override + public byte[] getByteContents() { + try { + return Util.getResourceContentsAsByteArray(getFile()); + } catch (JavaModelException e) { + return null; + } + } + + @Override + public char[] getCharContents() { + try { + return Util.getResourceContentsAsCharArray(getFile()); + } catch (JavaModelException e) { + return null; + } + } + + @Override + public String getEncoding() { + IFile resource = getFile(); + if (resource != null) { + try { + return resource.getCharset(); + } catch (CoreException e) { + // fall through + } + } + return null; + } + + private IFile getFile() { + if (this.file == null) { + this.file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(getPath())); + } + return this.file; + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSourceSearchParticipant.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSourceSearchParticipant.java new file mode 100644 index 0000000000..6d576b147b --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSourceSearchParticipant.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * 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 java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.search.DerivedSourceSearchParticipant; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchDocument; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.core.index.IndexLocation; +import org.eclipse.jdt.internal.core.search.IndexSelector; +import org.eclipse.jdt.internal.core.search.indexing.AbstractIndexer; +import org.eclipse.jdt.internal.core.search.indexing.IIndexConstants; +import org.eclipse.jdt.internal.core.search.matching.MatchLocator; +import org.eclipse.jdt.internal.core.search.matching.MethodPattern; +import org.eclipse.jdt.internal.core.search.matching.TypeDeclarationPattern; + +/** + * A test search participant for "Language X" ({@code .langx} files). + * Returns hardcoded index entries and delegates match location to + * {@link MatchLocator}. Used to verify that jdtls handlers correctly + * invoke contributed search participants. + */ +public class TestDerivedSourceSearchParticipant extends DerivedSourceSearchParticipant { + + public static final AtomicInteger beginSearchingCount = new AtomicInteger(); + public static final AtomicInteger doneSearchingCount = new AtomicInteger(); + public static final AtomicInteger selectIndexesCount = new AtomicInteger(); + public static final AtomicInteger locateMatchesCount = new AtomicInteger(); + public static final AtomicInteger indexDocumentCount = new AtomicInteger(); + public static final AtomicInteger locateCalleesCount = new AtomicInteger(); + public static final AtomicInteger getCompilationUnitCount = new AtomicInteger(); + + private final ThreadLocal indexSelector = new ThreadLocal<>(); + + public static void reset() { + beginSearchingCount.set(0); + doneSearchingCount.set(0); + selectIndexesCount.set(0); + locateMatchesCount.set(0); + indexDocumentCount.set(0); + locateCalleesCount.set(0); + getCompilationUnitCount.set(0); + } + + @Override + public String getDescription() { + return "Language X"; + } + + @Override + public void beginSearching() { + beginSearchingCount.incrementAndGet(); + this.indexSelector.remove(); + } + + @Override + public void doneSearching() { + doneSearchingCount.incrementAndGet(); + this.indexSelector.remove(); + } + + @Override + public SearchDocument getDocument(String documentPath) { + return new TestDerivedSearchDocument(documentPath, this); + } + + @Override + public void indexDocument(SearchDocument document, IPath indexLocation) { + indexDocumentCount.incrementAndGet(); + document.removeAllIndexEntries(); + new LangxIndexer(document).indexDocument(); + } + + @Override + public IPath[] selectIndexes(SearchPattern pattern, IJavaSearchScope scope) { + selectIndexesCount.incrementAndGet(); + IndexSelector selector = this.indexSelector.get(); + if (selector == null) { + selector = new IndexSelector(scope, pattern); + this.indexSelector.set(selector); + } + IndexLocation[] urls = selector.getIndexLocations(); + IPath[] paths = new IPath[urls.length]; + for (int i = 0; i < urls.length; i++) { + paths[i] = new Path(urls[i].getIndexFile().getPath()); + } + return paths; + } + + @Override + public void locateMatches(SearchDocument[] documents, SearchPattern pattern, + IJavaSearchScope scope, SearchRequestor requestor, + IProgressMonitor monitor) throws CoreException { + locateMatchesCount.incrementAndGet(); + SearchDocument[] langxDocs = filterLangxDocuments(documents); + if (langxDocs.length > 0) { + MatchLocator matchLocator = new MatchLocator(pattern, requestor, scope, monitor); + matchLocator.locateMatches(langxDocs); + } + } + + private static SearchDocument[] filterLangxDocuments(SearchDocument[] documents) { + int count = 0; + for (SearchDocument doc : documents) { + if (doc.getPath().endsWith(".langx")) { + count++; + } + } + if (count == documents.length) { + return documents; + } + SearchDocument[] filtered = new SearchDocument[count]; + int idx = 0; + for (SearchDocument doc : documents) { + if (doc.getPath().endsWith(".langx")) { + filtered[idx++] = doc; + } + } + return filtered; + } + + @Override + public SearchMatch[] locateCallees(IMember caller, SearchDocument document, + IProgressMonitor monitor) throws CoreException { + locateCalleesCount.incrementAndGet(); + return new SearchMatch[0]; + } + + @Override + public org.eclipse.jdt.core.ICompilationUnit getCompilationUnit( + org.eclipse.core.resources.IFile file) { + getCompilationUnitCount.incrementAndGet(); + return null; + } + + /** + * Hardcoded indexer for Language X. Adds a fixed type declaration + * and method declaration for any {@code .langx} document. + */ + private static class LangxIndexer extends AbstractIndexer { + + LangxIndexer(SearchDocument document) { + super(document); + } + + @Override + public void indexDocument() { + addIndexEntry(IIndexConstants.TYPE_DECL, + TypeDeclarationPattern.createIndexKey( + 0, "LangxType".toCharArray(), + "java".toCharArray(), + new char[0][], false)); + addIndexEntry(IIndexConstants.METHOD_DECL, + MethodPattern.createIndexKey( + "langxMethod".toCharArray(), 0)); + } + } +}