From 2e3e2814c344b1298c8829fa20dec9fe0064fb30 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Fri, 5 Jun 2026 00:40:15 -0400 Subject: [PATCH] Use SearchEngine.getSearchParticipants() for non-Java language search Replace getDefaultSearchParticipant() with getSearchParticipants() across all jdtls search call sites so that contributed DerivedSourceSearchParticipants (e.g., Kotlin, Scala) are included in reference, implementation, hover, code lens, workspace symbol, and type hierarchy searches. Call sites updated: - ReferencesHandler.search() - CodeLensHandler.findReferences() - HoverInfoProvider.hasMatchInUnit() - JDTUtils.findElementsAtSelection() - ImplementationCollector (method + type implementations) - TypeHierarchyHandler (supertypes + subtypes) - WorkspaceSymbolHandler.search() Includes a test search participant ("Language X") registered for .langx files to verify that contributed participants are invoked during handler searches. --- .../core/internal/BaseJDTLanguageServer.java | 9 +- .../ls/core/internal/HoverInfoProvider.java | 39 +- .../jdt/ls/core/internal/JDTUtils.java | 101 +++++ .../jdt/ls/core/internal/SearchUtils.java | 18 + .../internal/handlers/CodeLensHandler.java | 3 +- .../handlers/ImplementationCollector.java | 65 +++- .../internal/handlers/JDTLanguageServer.java | 6 + .../NavigateToDeclarationHandler.java | 12 +- .../handlers/NavigateToDefinitionHandler.java | 2 +- .../NavigateToTypeDefinitionHandler.java | 70 +++- .../internal/handlers/ReferencesHandler.java | 13 +- .../handlers/TypeHierarchyHandler.java | 270 +++++++++++++- .../handlers/WorkspaceSymbolHandler.java | 133 +++++++ org.eclipse.jdt.ls.tests/plugin.xml | 14 + .../eclipse/hello/src/java/LangxType.langx | 4 + .../DerivedSourceSearchParticipantsTest.java | 345 ++++++++++++++++++ .../handlers/TestDerivedSearchDocument.java | 69 ++++ .../TestDerivedSourceSearchParticipant.java | 177 +++++++++ 18 files changed, 1315 insertions(+), 35 deletions(-) create mode 100644 org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/LangxType.langx create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DerivedSourceSearchParticipantsTest.java create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSearchDocument.java create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TestDerivedSourceSearchParticipant.java diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java index b83b5595d9..c6854ac8cc 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/BaseJDTLanguageServer.java @@ -84,7 +84,14 @@ protected void toggleCapability(boolean enabled, String id, String capability, O } protected CompletableFuture computeAsync(Function code) { - return CompletableFutures.computeAsync(cc -> code.apply(toMonitor(cc))); + return CompletableFutures.computeAsync(cc -> code.apply(toMonitor(cc))) + .whenComplete((result, error) -> { + if (error != null) { + JavaLanguageServerPlugin.logException( + "Unhandled exception in LSP request handler", + error); + } + }); } protected IProgressMonitor toMonitor(CancelChecker checker) { diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/HoverInfoProvider.java index e8a5247e86..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)); + } + } +}