diff --git a/.gitignore b/.gitignore
index bb536c5ee2b..c003a03648e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ Snap.*
# pomless
.polyglot.*
/.project
+.idea/
+.claude-tmp/
diff --git a/org.eclipse.jdt.core.tests.model/plugin.xml b/org.eclipse.jdt.core.tests.model/plugin.xml
index 0161a1ef4c1..0f9a99cbb86 100644
--- a/org.eclipse.jdt.core.tests.model/plugin.xml
+++ b/org.eclipse.jdt.core.tests.model/plugin.xml
@@ -119,6 +119,21 @@
file-extensions="kt">
+
+
+
+
+
+
+
+
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java
index 35eef6b706c..102de7b4839 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java
@@ -215,6 +215,9 @@ private static Class[] getAllTestClasses() {
// Create search participant tests
SearchParticipantTests.class,
+ // Derived source search participant tests
+ DerivedSourceSearchParticipantTests.class,
+
// Class file tests
ClassFileTests.class,
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/DerivedSourceSearchParticipantTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/DerivedSourceSearchParticipantTests.java
new file mode 100644
index 00000000000..5fa945cf03e
--- /dev/null
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/DerivedSourceSearchParticipantTests.java
@@ -0,0 +1,241 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Eclipse Foundation and others.
+ *
+ * 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.core.tests.model;
+
+import junit.framework.Test;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry;
+
+/**
+ * Tests for the {@code org.eclipse.jdt.core.searchParticipant} extension point
+ * and the {@link SearchParticipantRegistry}.
+ */
+public class DerivedSourceSearchParticipantTests extends ModifyingResourceTests {
+
+ IJavaProject project;
+
+ public DerivedSourceSearchParticipantTests(String name) {
+ super(name);
+ }
+
+ public static Test suite() {
+ return buildModelTestSuite(DerivedSourceSearchParticipantTests.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ TestDerivedSourceSearchParticipant.reset();
+ SearchParticipantRegistry.reset();
+ this.project = createJavaProject("DSP", new String[] {"src"}, "bin");
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ deleteProject("DSP");
+ this.project = null;
+ super.tearDown();
+ }
+
+ /**
+ * Verifies that the registry discovers the {@code .langx} extension
+ * from the test plugin's contribution.
+ */
+ public void testRegistryHasParticipantForLangx() {
+ assertTrue("Registry should have participant for langx",
+ SearchParticipantRegistry.hasParticipant("langx"));
+ }
+
+ /**
+ * Verifies that the registry does not report a participant for
+ * an unregistered extension.
+ */
+ public void testRegistryNoParticipantForUnknownExtension() {
+ assertFalse("Registry should not have participant for unknown_ext",
+ SearchParticipantRegistry.hasParticipant("unknown_ext"));
+ }
+
+ /**
+ * Verifies that {@code getParticipant("langx")} returns a non-null
+ * participant of the correct type and that the instance is reused.
+ */
+ public void testRegistryGetParticipantSingleton() {
+ SearchParticipant p1 = SearchParticipantRegistry.getParticipant("langx");
+ assertNotNull("Should return a participant for langx", p1);
+ assertTrue("Should be a TestDerivedSourceSearchParticipant",
+ p1 instanceof TestDerivedSourceSearchParticipant);
+ SearchParticipant p2 = SearchParticipantRegistry.getParticipant("langx");
+ assertSame("Same instance should be returned on second call", p1, p2);
+ assertEquals("Exactly one instance should be created",
+ 1, TestDerivedSourceSearchParticipant.instanceCount.get());
+ }
+
+ /**
+ * Verifies that {@code getContributedParticipants()} includes the
+ * test participant.
+ */
+ public void testGetContributedParticipants() {
+ SearchParticipant[] contributed = SearchParticipantRegistry.getContributedParticipants();
+ assertTrue("Should have at least one contributed participant",
+ contributed.length >= 1);
+ boolean found = false;
+ for (SearchParticipant p : contributed) {
+ if (p instanceof TestDerivedSourceSearchParticipant) {
+ found = true;
+ break;
+ }
+ }
+ assertTrue("Contributed participants should include TestDerivedSourceSearchParticipant",
+ found);
+ }
+
+ /**
+ * Verifies that {@link SearchEngine#getSearchParticipants()} returns both
+ * the default participant and contributed participants.
+ */
+ public void testGetSearchParticipants() {
+ SearchParticipant[] participants = SearchEngine.getSearchParticipants();
+ assertTrue("Should have at least 2 participants (default + contributed)",
+ participants.length >= 2);
+ boolean hasDefault = false;
+ boolean hasContributed = false;
+ SearchParticipant defaultP = SearchEngine.getDefaultSearchParticipant();
+ for (SearchParticipant p : participants) {
+ if (p.getClass() == defaultP.getClass()) {
+ hasDefault = true;
+ }
+ if (p instanceof TestDerivedSourceSearchParticipant) {
+ hasContributed = true;
+ }
+ }
+ assertTrue("Should include the default Java search participant", hasDefault);
+ assertTrue("Should include the contributed test participant", hasContributed);
+ }
+
+ /**
+ * Verifies that {@code getFileExtension()} correctly extracts extensions.
+ */
+ public void testGetFileExtension() {
+ assertEquals("kt", SearchParticipantRegistry.getFileExtension("Foo.kt"));
+ assertEquals("langx", SearchParticipantRegistry.getFileExtension("Bar.langx"));
+ assertEquals("java", SearchParticipantRegistry.getFileExtension("Baz.java"));
+ assertNull(SearchParticipantRegistry.getFileExtension("noextension"));
+ }
+
+ /**
+ * Verifies that adding a {@code .langx} file to a source folder triggers
+ * the search participant's {@code indexDocument()} method via the
+ * automatic indexing pipeline.
+ */
+ public void testIndexingTriggeredForDerivedSourceFile() throws CoreException {
+ createFile(
+ "/DSP/src/Hello.langx",
+ "public class Hello {\n" +
+ " public void greet() {}\n" +
+ "}"
+ );
+ waitUntilIndexesReady();
+ assertTrue("indexDocument should have been called at least once",
+ TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() > 0);
+ }
+
+ /**
+ * Verifies that adding a second {@code .langx} file triggers additional
+ * indexing calls.
+ */
+ public void testIndexingTriggeredForMultipleDerivedSourceFiles() throws CoreException {
+ createFile(
+ "/DSP/src/Alpha.langx",
+ "public class Alpha {\n" +
+ " public int value() { return 1; }\n" +
+ "}"
+ );
+ createFile(
+ "/DSP/src/Beta.langx",
+ "public class Beta {\n" +
+ " public int value() { return 2; }\n" +
+ "}"
+ );
+ waitUntilIndexesReady();
+ assertTrue("indexDocument should have been called at least twice",
+ TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() >= 2);
+ }
+
+ /**
+ * Verifies that creating a {@code .langx} file in a subfolder/package
+ * triggers indexing via the add-folder-to-index path.
+ */
+ public void testIndexingInSubPackage() throws CoreException {
+ createFolder("/DSP/src/pkg");
+ createFile(
+ "/DSP/src/pkg/InPackage.langx",
+ "package pkg;\n" +
+ "public class InPackage {}"
+ );
+ waitUntilIndexesReady();
+ assertTrue("indexDocument should have been called for file in subpackage",
+ TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() > 0);
+ }
+
+ /**
+ * Verifies that deleting a {@code .langx} file does not crash
+ * and that the index is updated.
+ */
+ public void testDeletionOfDerivedSourceFile() throws CoreException {
+ createFile(
+ "/DSP/src/ToDelete.langx",
+ "public class ToDelete {}"
+ );
+ waitUntilIndexesReady();
+
+ // delete the file — should not throw
+ deleteFile("/DSP/src/ToDelete.langx");
+ waitUntilIndexesReady();
+ // If we get here without an exception, the delta processor handled removal correctly
+ }
+
+ /**
+ * Verifies that regular {@code .java} files are not routed to the
+ * derived source participant.
+ */
+ public void testJavaFilesNotRoutedToParticipant() throws CoreException {
+ createFile(
+ "/DSP/src/Regular.java",
+ "public class Regular {}"
+ );
+ waitUntilIndexesReady();
+ assertEquals("indexDocument should not be called for .java files",
+ 0, TestDerivedSourceSearchParticipant.indexDocumentCallCount.get());
+ }
+
+ /**
+ * Verifies that the registry reset clears cached participants
+ * and forces re-loading on next access.
+ */
+ public void testRegistryReset() {
+ SearchParticipant before = SearchParticipantRegistry.getParticipant("langx");
+ assertNotNull(before);
+ TestDerivedSourceSearchParticipant.reset();
+ SearchParticipantRegistry.reset();
+
+ SearchParticipant after = SearchParticipantRegistry.getParticipant("langx");
+ assertNotNull(after);
+ assertNotSame("After reset, a new instance should be created", before, after);
+ assertEquals("New instance should have been created after reset",
+ 1, TestDerivedSourceSearchParticipant.instanceCount.get());
+ }
+}
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSearchDocument.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSearchDocument.java
new file mode 100644
index 00000000000..657c5883ebd
--- /dev/null
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSearchDocument.java
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Eclipse Foundation and others.
+ *
+ * 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.core.tests.model;
+
+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;
+
+/**
+ * A search document for derived source files (e.g. {@code .langx}).
+ * Reads file contents from the workspace.
+ */
+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.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSourceSearchParticipant.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSourceSearchParticipant.java
new file mode 100644
index 00000000000..546a8809c25
--- /dev/null
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/TestDerivedSourceSearchParticipant.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Eclipse Foundation and others.
+ *
+ * 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.core.tests.model;
+
+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.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchDocument;
+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.search.indexing.SourceIndexer;
+
+/**
+ * A test search participant for {@code .langx} files. Delegates indexing to
+ * the default Java source indexer (treating {@code .langx} content as Java syntax
+ * for testing purposes).
+ */
+public class TestDerivedSourceSearchParticipant extends SearchParticipant {
+
+ public static final AtomicInteger indexDocumentCallCount = new AtomicInteger(0);
+ public static final AtomicInteger instanceCount = new AtomicInteger(0);
+
+ public TestDerivedSourceSearchParticipant() {
+ instanceCount.incrementAndGet();
+ }
+
+ @Override
+ public SearchDocument getDocument(String documentPath) {
+ return new TestDerivedSearchDocument(documentPath, this);
+ }
+
+ @Override
+ public void indexDocument(SearchDocument document, IPath indexLocation) {
+ indexDocumentCallCount.incrementAndGet();
+ document.removeAllIndexEntries();
+ // delegate to the Java source indexer for testing
+ new SourceIndexer(document).indexDocument();
+ }
+
+ @Override
+ public void locateMatches(SearchDocument[] documents, SearchPattern pattern,
+ IJavaSearchScope scope, SearchRequestor requestor,
+ IProgressMonitor monitor) throws CoreException {
+ // no-op for now — index population is what we test
+ }
+
+ @Override
+ public IPath[] selectIndexes(SearchPattern query, IJavaSearchScope scope) {
+ return new IPath[0];
+ }
+
+ public static void reset() {
+ indexDocumentCallCount.set(0);
+ instanceCount.set(0);
+ }
+}
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DeltaProcessor.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DeltaProcessor.java
index ba2d95de595..fa1172c947d 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DeltaProcessor.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/DeltaProcessor.java
@@ -49,6 +49,7 @@
import org.eclipse.jdt.internal.core.search.AbstractSearchScope;
import org.eclipse.jdt.internal.core.search.JavaWorkspaceScope;
import org.eclipse.jdt.internal.core.search.indexing.IndexManager;
+import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry;
import org.eclipse.jdt.internal.core.util.Util;
/**
@@ -2799,6 +2800,9 @@ private void updateIndex(Openable element, IResourceDelta delta) {
if (org.eclipse.jdt.internal.core.util.Util.isJavaLikeFileName(name)) {
Openable cu = (Openable)pkg.getCompilationUnit(name);
updateIndex(cu, child);
+ } else if (org.eclipse.jdt.internal.core.util.Util.isJavaDerivedFileName(name)
+ && SearchParticipantRegistry.hasParticipant(SearchParticipantRegistry.getFileExtension(name))) {
+ updateDerivedSourceIndex((IFile) resource, child, indexManager);
}
} else if (org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(name)) {
Openable classFile = (Openable)pkg.getClassFile(name);
@@ -2866,6 +2870,42 @@ private void updateIndex(Openable element, IResourceDelta delta) {
}
}
}
+ /**
+ * Updates the search index for a derived source file (e.g. .kt, .kts) whose extension
+ * is registered via the {@code org.eclipse.jdt.core.searchParticipant} extension point.
+ *
+ * Derived source files are indexed through their registered {@link org.eclipse.jdt.core.search.SearchParticipant SearchParticipant},
+ * which is responsible for parsing the file and producing index entries (type declarations,
+ * references, etc.) that enable cross-language search, type hierarchy, and call hierarchy.
+ *
+ * The indexing logic mirrors
+ * {@link #updateIndex(Openable, IResourceDelta) updateIndex} for compilation units:
+ *
+ * - ADDED — schedule the file for indexing via its search participant
+ * - CHANGED — re-index only if the file content or encoding actually changed
+ * (falls through to ADDED)
+ * - REMOVED — remove the file's entries from the index
+ *
+ *
+ * @param file the derived source file that changed
+ * @param delta the resource delta describing the change
+ * @param indexManager the index manager to schedule indexing operations on
+ */
+ private void updateDerivedSourceIndex(IFile file, IResourceDelta delta, IndexManager indexManager) {
+ switch (delta.getKind()) {
+ case IResourceDelta.CHANGED :
+ int flags = delta.getFlags();
+ if ((flags & IResourceDelta.CONTENT) == 0 && (flags & IResourceDelta.ENCODING) == 0)
+ break;
+ // $FALL-THROUGH$
+ case IResourceDelta.ADDED :
+ indexManager.addDerivedSource(file, file.getProject().getFullPath());
+ break;
+ case IResourceDelta.REMOVED :
+ indexManager.remove(Util.relativePath(file.getFullPath(), 1/*remove project segment*/), file.getProject().getFullPath());
+ break;
+ }
+ }
/*
* Update Java Model given some delta
*/
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaModelManager.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaModelManager.java
index 83811e910af..ee1aad7fb4c 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaModelManager.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaModelManager.java
@@ -80,6 +80,7 @@
import org.eclipse.jdt.internal.core.search.IRestrictedAccessTypeRequestor;
import org.eclipse.jdt.internal.core.search.JavaWorkspaceScope;
import org.eclipse.jdt.internal.core.search.indexing.IndexManager;
+import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry;
import org.eclipse.jdt.internal.core.search.processing.IJob;
import org.eclipse.jdt.internal.core.search.processing.JobManager;
import org.eclipse.jdt.internal.core.util.DeduplicationUtil;
@@ -5531,6 +5532,9 @@ public void shutdown () {
contentTypeManager.removeContentTypeChangeListener(this);
}
+ // Stop listening to search participant extension changes
+ SearchParticipantRegistry.disposeInstance();
+
// Stop indexing
if (this.indexManager != null) {
this.indexManager.shutdown();
diff --git a/org.eclipse.jdt.core/plugin.properties b/org.eclipse.jdt.core/plugin.properties
index 401724a44d5..29bfc2511af 100644
--- a/org.eclipse.jdt.core/plugin.properties
+++ b/org.eclipse.jdt.core/plugin.properties
@@ -26,6 +26,7 @@ compilationParticipantsName=Compilation Participants
compilationUnitResolverName=Compilation Unit Resolver
completionEngineProviderName=Completion Engine Provider
javaSearchDelegateName=Java Search Delegate
+searchParticipantsName=Search Participants
annotationProcessorManagerName=Java 6 Annotation Processor Manager
javaTaskName=Java Task
javaPropertiesName=Java Properties File
diff --git a/org.eclipse.jdt.core/plugin.xml b/org.eclipse.jdt.core/plugin.xml
index d21b654c2a2..822ea2bfe15 100644
--- a/org.eclipse.jdt.core/plugin.xml
+++ b/org.eclipse.jdt.core/plugin.xml
@@ -86,6 +86,13 @@
id="javaSearchDelegate"
schema="schema/javaSearchDelegate.exsd"/>
+
+
+
+
+
diff --git a/org.eclipse.jdt.core/schema/searchParticipant.exsd b/org.eclipse.jdt.core/schema/searchParticipant.exsd
new file mode 100644
index 00000000000..1fca02542aa
--- /dev/null
+++ b/org.eclipse.jdt.core/schema/searchParticipant.exsd
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+ This extension point allows clients to contribute search participants for non-Java source files that are registered under the <code>org.eclipse.jdt.core.javaDerivedSource</code> content type. When files with the specified extensions are discovered in source folders, JDT Core's indexing pipeline will route them to the registered search participant for indexing. The participant's indexes are also queried during search operations.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Definition of a search participant for non-Java source files.
+
+
+
+
+
+
+ The class that implements this search participant. This class must extend <code>org.eclipse.jdt.core.search.SearchParticipant</code> with a public 0-argument constructor.
+
+
+
+
+
+
+
+
+
+ A unique identifier for this search participant.
+
+
+
+
+
+
+ A comma-separated list of file extensions (without dots) that this participant handles. These extensions must be registered under the <code>org.eclipse.jdt.core.javaDerivedSource</code> content type. For example: <code>kt,kts</code>.
+
+
+
+
+
+
+ The LSP language identifier for the source files handled by this participant (e.g. <code>kotlin</code>, <code>scala</code>). Used by language server handlers to tag hover content and other language-specific responses. If omitted, defaults to the first file extension.
+
+
+
+
+
+
+
+
+
+
+
+ 3.46
+
+
+
+
+
+
+
+
+ Example of a declaration of a <code>searchParticipant</code>: <pre>
+<extension
+ point="org.eclipse.jdt.core.searchParticipant">
+ <searchParticipant
+ class="co.karellen.jdt.kotlin.search.KotlinSearchParticipant"
+ id="co.karellen.jdt.kotlin.searchParticipant"
+ fileExtensions="kt,kts"
+ languageId="kotlin">
+ </searchParticipant>
+</extension>
+</pre>
+
+
+
+
+
+
+
+
+ The contributed class must extend <code>org.eclipse.jdt.core.search.SearchParticipant</code>. It will be called by JDT Core's <code>IndexManager</code> when files matching the declared extensions are discovered in source folders. The participant is responsible for parsing the file content and calling <code>SearchDocument.addIndexEntry()</code> to populate the search index.
+
+
+
+
+
+
+
+
+ JDT Core provides the default Java search participant via <code>SearchEngine.getDefaultSearchParticipant()</code>, which handles <code>.java</code> and <code>.class</code> files. This extension point is for additional non-Java languages.
+
+
+
+
+
+
+
+
+ Copyright (c) 2026 Eclipse Foundation and others.<br>
+
+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
+<a href="https://www.eclipse.org/legal/epl-2.0">https://www.eclipse.org/legal/epl-2.0</a>/
+
+SPDX-License-Identifier: EPL-2.0
+
+
+
+
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchEngine.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchEngine.java
index e60c0618e41..6b62391e7d7 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchEngine.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchEngine.java
@@ -545,6 +545,26 @@ public static SearchParticipant getDefaultSearchParticipant() {
return BasicSearchEngine.getDefaultSearchParticipant();
}
+ /**
+ * Returns an array of search participants that includes the default Java search
+ * participant followed by any participants contributed via the
+ * {@code org.eclipse.jdt.core.searchParticipant} extension point.
+ *
+ * If no extension point contributions exist, the returned array contains only
+ * the default Java search participant (equivalent to wrapping
+ * {@link #getDefaultSearchParticipant()} in a single-element array).
+ *
+ * The default Java search participant is a new instance on each call (consistent
+ * with {@link #getDefaultSearchParticipant()}). Contributed participants are
+ * singleton instances shared across calls.
+ *
+ * @return array of search participants, never {@code null} or empty
+ * @since 3.46
+ */
+ public static SearchParticipant[] getSearchParticipants() {
+ return BasicSearchEngine.getSearchParticipants();
+ }
+
/**
* Searches for the Java element determined by the given signature. The signature
* can be incomplete. For example, a call like
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchParticipant.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchParticipant.java
index 4cd0c0c6b28..0f3bc9e74c1 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchParticipant.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/core/search/SearchParticipant.java
@@ -13,11 +13,13 @@
*******************************************************************************/
package org.eclipse.jdt.core.search;
+import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
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.ICompilationUnit;
import org.eclipse.jdt.internal.core.JavaModel;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.index.FileIndexLocation;
@@ -196,6 +198,59 @@ public void indexResolvedDocument(SearchDocument document, IPath indexLocation)
*/
public abstract void locateMatches(SearchDocument[] documents, SearchPattern pattern, IJavaSearchScope scope, SearchRequestor requestor, IProgressMonitor monitor) throws CoreException;
+ /**
+ * Locates methods and types invoked by the given member. Called by the call
+ * hierarchy engine when Java AST-based callee analysis is not available
+ * (i.e., the member's source is not Java).
+ *
+ *
Each returned {@link SearchMatch} represents a call site within the
+ * member's body:
+ *
+ * - {@link SearchMatch#getElement()} — an {@link org.eclipse.jdt.core.IMember IMember}
+ * representing the callee. At minimum,
+ * {@link org.eclipse.jdt.core.IJavaElement#getElementName() getElementName()} and
+ * {@link org.eclipse.jdt.core.IJavaElement#getElementType() getElementType()} must
+ * return meaningful values. The call hierarchy engine will attempt to resolve this
+ * to a full declaration via declaration search.
+ * - {@link SearchMatch#getOffset()} / {@link SearchMatch#getLength()} —
+ * the call site location in the caller's source.
+ * - {@link SearchMatch#getResource()} — the caller's resource.
+ *
+ *
+ * The default implementation returns an empty array. Subclasses that
+ * support non-Java languages should override this method to enable outgoing
+ * call hierarchy for their language's source files.
+ *
+ * @param caller the member whose callees are requested
+ * @param document the search document for the caller's source file
+ * @param monitor progress monitor, or {@code null}
+ * @return array of search matches representing call sites (never null)
+ * @throws CoreException if an error occurs during callee analysis
+ * @since 3.46
+ */
+ public SearchMatch[] locateCallees(org.eclipse.jdt.core.IMember caller, SearchDocument document,
+ IProgressMonitor monitor) throws CoreException {
+ return new SearchMatch[0];
+ }
+
+ /**
+ * Returns an ICompilationUnit for the given source file, or null if this
+ * participant does not provide structured models. Called by language
+ * servers to resolve non-Java source files to type roots for features
+ * like document symbols, hover, go-to-definition, and code lenses.
+ *
+ *
The default implementation returns null. Subclasses that provide
+ * structured models for their language should override this to return
+ * a compilation unit populated with type/method/field children.
+ *
+ * @param file the workspace file
+ * @return compilation unit, or null
+ * @since 3.46
+ */
+ public ICompilationUnit getCompilationUnit(IFile file) {
+ return null;
+ }
+
/**
* Removes the index for a given path.
*
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/BasicSearchEngine.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/BasicSearchEngine.java
index 7d549e0663d..7968c9e62a3 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/BasicSearchEngine.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/BasicSearchEngine.java
@@ -64,6 +64,7 @@
import org.eclipse.jdt.internal.core.SourceMethodElementInfo;
import org.eclipse.jdt.internal.core.search.indexing.IIndexConstants;
import org.eclipse.jdt.internal.core.search.indexing.IndexManager;
+import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry;
import org.eclipse.jdt.internal.core.search.matching.*;
import org.eclipse.jdt.internal.core.util.Messages;
@@ -299,6 +300,21 @@ public static SearchParticipant getDefaultSearchParticipant() {
return new JavaSearchParticipant();
}
+ /**
+ * Returns the default participant plus all contributed participants.
+ */
+ public static SearchParticipant[] getSearchParticipants() {
+ SearchParticipant defaultParticipant = getDefaultSearchParticipant();
+ SearchParticipant[] contributed = SearchParticipantRegistry.getContributedParticipants();
+ if (contributed.length == 0) {
+ return new SearchParticipant[] { defaultParticipant };
+ }
+ SearchParticipant[] result = new SearchParticipant[1 + contributed.length];
+ result[0] = defaultParticipant;
+ System.arraycopy(contributed, 0, result, 1, contributed.length);
+ return result;
+ }
+
public static String getMatchRuleString(final int matchRule) {
if (matchRule == 0) {
return "R_EXACT_MATCH"; //$NON-NLS-1$
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/AddFolderToIndex.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/AddFolderToIndex.java
index d657040f3f6..68925b6791d 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/AddFolderToIndex.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/AddFolderToIndex.java
@@ -10,6 +10,7 @@
*
* Contributors:
* IBM Corporation - initial API and implementation
+ * Arcadiy Ivanov - javaDerivedSource indexing support
*******************************************************************************/
package org.eclipse.jdt.internal.core.search.indexing;
@@ -70,6 +71,9 @@ public boolean visit(IResourceProxy proxy) /* throws CoreException */{
if (proxy.getType() == IResource.FILE) {
if (org.eclipse.jdt.internal.core.util.Util.isJavaLikeFileName(proxy.getName()))
indexManager.addSource((IFile) proxy.requestResource(), container, parser);
+ else if (org.eclipse.jdt.internal.core.util.Util.isJavaDerivedFileName(proxy.getName())
+ && SearchParticipantRegistry.hasParticipant(SearchParticipantRegistry.getFileExtension(proxy.getName())))
+ indexManager.addDerivedSource((IFile) proxy.requestResource(), container);
return false;
}
return true;
@@ -88,6 +92,11 @@ public boolean visit(IResourceProxy proxy) /* throws CoreException */{
IResource resource = proxy.requestResource();
if (!Util.isExcluded(resource, AddFolderToIndex.this.inclusionPatterns, AddFolderToIndex.this.exclusionPatterns))
indexManager.addSource((IFile)resource, container, parser);
+ } else if (org.eclipse.jdt.internal.core.util.Util.isJavaDerivedFileName(proxy.getName())
+ && SearchParticipantRegistry.hasParticipant(SearchParticipantRegistry.getFileExtension(proxy.getName()))) {
+ IResource resource = proxy.requestResource();
+ if (!Util.isExcluded(resource, AddFolderToIndex.this.inclusionPatterns, AddFolderToIndex.this.exclusionPatterns))
+ indexManager.addDerivedSource((IFile)resource, container);
}
return false;
case IResource.FOLDER :
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexAllProject.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexAllProject.java
index a57c717a0ba..0f706e05198 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexAllProject.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexAllProject.java
@@ -10,6 +10,7 @@
*
* Contributors:
* IBM Corporation - initial API and implementation
+ * Arcadiy Ivanov - javaDerivedSource indexing support
*******************************************************************************/
package org.eclipse.jdt.internal.core.search.indexing;
@@ -105,6 +106,7 @@ public boolean execute(IProgressMonitor progressMonitor) {
String[] paths = index.queryDocumentNames(""); // all file names //$NON-NLS-1$
int max = paths == null ? 0 : paths.length;
final SimpleLookupTable indexedFileNames = new SimpleLookupTable(max == 0 ? 33 : max + 11);
+ final SimpleLookupTable derivedFileNames = new SimpleLookupTable(11);
final String OK = "OK"; //$NON-NLS-1$
final String DELETED = "DELETED"; //$NON-NLS-1$
if (paths != null) {
@@ -151,6 +153,13 @@ public boolean visit(IResourceProxy proxy) {
if (Util.isExcluded(file, inclusionPatterns, exclusionPatterns))
return false;
indexedFileNames.put(Util.relativePath(file.getFullPath(), 1/*remove project segment*/), file);
+ } else if (org.eclipse.jdt.internal.core.util.Util.isJavaDerivedFileName(proxy.getName())
+ && SearchParticipantRegistry.hasParticipant(SearchParticipantRegistry.getFileExtension(proxy.getName()))) {
+ IFile file = (IFile) proxy.requestResource();
+ if (exclusionPatterns != null || inclusionPatterns != null)
+ if (Util.isExcluded(file, inclusionPatterns, exclusionPatterns))
+ return false;
+ derivedFileNames.put(Util.relativePath(file.getFullPath(), 1/*remove project segment*/), file);
}
return false;
case IResource.FOLDER :
@@ -188,6 +197,20 @@ public boolean visit(IResourceProxy proxy) throws CoreException {
|| indexLastModified < EFS.getStore(location).fetchInfo().getLastModified()
? (Object) file
: (Object) OK);
+ } else if (org.eclipse.jdt.internal.core.util.Util.isJavaDerivedFileName(proxy.getName())
+ && SearchParticipantRegistry.hasParticipant(SearchParticipantRegistry.getFileExtension(proxy.getName()))) {
+ IFile file = (IFile) proxy.requestResource();
+ URI location = file.getLocationURI();
+ if (location == null) return false;
+ if (exclusionPatterns != null || inclusionPatterns != null)
+ if (Util.isExcluded(file, inclusionPatterns, exclusionPatterns))
+ return false;
+ String relativePathString = Util.relativePath(file.getFullPath(), 1/*remove project segment*/);
+ boolean needsIndexing = indexedFileNames.get(relativePathString) == null
+ || indexLastModified < EFS.getStore(location).fetchInfo().getLastModified();
+ // clear DELETED marker so the indexedFileNames loop does not issue a spurious remove()
+ indexedFileNames.put(relativePathString, OK);
+ derivedFileNames.put(relativePathString, needsIndexing ? (Object) file : (Object) OK);
}
return false;
case IResource.FOLDER :
@@ -224,6 +247,23 @@ public boolean visit(IResourceProxy proxy) throws CoreException {
}
}
+ // index derived source files via their registered search participants
+ names = derivedFileNames.keyTable;
+ values = derivedFileNames.valueTable;
+ for (int i = 0, namesLength = names.length; i < namesLength; i++) {
+ String name = (String) names[i];
+ if (name != null) {
+ if (this.isCancelled) return false;
+ Object value = values[i];
+ if (value != OK) {
+ if (value == DELETED)
+ this.manager.remove(name, this.containerPath);
+ else
+ this.manager.addDerivedSource((IFile) value, this.containerPath);
+ }
+ }
+ }
+
// request to save index when all cus have been indexed... also sets state to SAVED_STATE
this.manager.request(new SaveIndex(this.containerPath, this.manager));
} catch (CoreException | IOException e) {
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexManager.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexManager.java
index 1b3dadde162..aef790e2c72 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexManager.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/IndexManager.java
@@ -258,6 +258,20 @@ public void addSource(IFile resource, IPath containerPath, SourceElementParser p
IndexLocation indexLocation = computeIndexLocation(containerPath);
scheduleDocumentIndexing(document, containerPath, indexLocation, participant);
}
+/**
+ * Trigger addition of a derived source file to an index via its registered search participant.
+ * Note: the actual operation is performed in background
+ */
+public void addDerivedSource(IFile resource, IPath containerPath) {
+ if (JavaCore.getPlugin() == null) return;
+ String extension = SearchParticipantRegistry.getFileExtension(resource.getName());
+ if (extension == null) return;
+ SearchParticipant participant = SearchParticipantRegistry.getParticipant(extension);
+ if (participant == null) return;
+ SearchDocument document = participant.getDocument(resource.getFullPath().toString());
+ IndexLocation indexLocation = computeIndexLocation(containerPath);
+ scheduleDocumentIndexing(document, containerPath, indexLocation, participant);
+}
/**
* Removes unused indexes from disk.
*/
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SearchParticipantRegistry.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SearchParticipantRegistry.java
new file mode 100644
index 00000000000..b92c748ffb8
--- /dev/null
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/indexing/SearchParticipantRegistry.java
@@ -0,0 +1,235 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Eclipse Foundation and others.
+ *
+ * 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.internal.core.search.indexing;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.IExtension;
+import org.eclipse.core.runtime.IExtensionPoint;
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.IRegistryEventListener;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.search.SearchParticipant;
+
+/**
+ * Registry for search participants contributed via the
+ * {@code org.eclipse.jdt.core.searchParticipant} extension point.
+ *
+ * Each contributed participant declares a set of file extensions it handles.
+ * The registry maps file extensions to eagerly-instantiated participant instances.
+ *
+ * The registry listens for extension additions and removals via
+ * {@link IRegistryEventListener}, so dynamically loaded or unloaded bundles
+ * are reflected automatically without restart.
+ */
+public class SearchParticipantRegistry implements IRegistryEventListener {
+
+ private static final String EXTENSION_POINT_ID = JavaCore.PLUGIN_ID + ".searchParticipant"; //$NON-NLS-1$
+ private static final String ELEMENT_NAME = "searchParticipant"; //$NON-NLS-1$
+ private static final String ATTR_CLASS = "class"; //$NON-NLS-1$
+ private static final String ATTR_ID = "id"; //$NON-NLS-1$
+ private static final String ATTR_FILE_EXTENSIONS = "fileExtensions"; //$NON-NLS-1$
+ private static final String ATTR_LANGUAGE_ID = "languageId"; //$NON-NLS-1$
+
+ private static final SearchParticipantRegistry INSTANCE = new SearchParticipantRegistry();
+
+ /** file extension → instantiated participant. Guarded by {@code synchronized(INSTANCE)}. */
+ private final Map participantsByExtension = new HashMap<>();
+
+ /** file extension → LSP language identifier. Guarded by {@code synchronized(INSTANCE)}. */
+ private final Map languageIdsByExtension = new HashMap<>();
+
+ private SearchParticipantRegistry() {
+ Platform.getExtensionRegistry().addListener(this, EXTENSION_POINT_ID);
+ synchronized (this) {
+ load();
+ }
+ }
+
+ /** Must be called while holding {@code synchronized(this)}. */
+ private void load() {
+ IConfigurationElement[] elements = Platform.getExtensionRegistry()
+ .getConfigurationElementsFor(EXTENSION_POINT_ID);
+ // group config elements by identity so each participant class is instantiated once
+ Map> extensionsByConfig = new HashMap<>();
+ for (IConfigurationElement element : elements) {
+ if (!ELEMENT_NAME.equals(element.getName())) continue;
+ String fileExtensions = element.getAttribute(ATTR_FILE_EXTENSIONS);
+ if (fileExtensions == null || fileExtensions.isBlank()) continue;
+ for (String ext : fileExtensions.split(",")) { //$NON-NLS-1$
+ String trimmed = ext.trim().toLowerCase();
+ if (!trimmed.isEmpty()) {
+ extensionsByConfig.computeIfAbsent(element, k -> new LinkedHashSet<>()).add(trimmed);
+ }
+ }
+ }
+ for (Map.Entry> entry : extensionsByConfig.entrySet()) {
+ IConfigurationElement config = entry.getKey();
+ Set exts = entry.getValue();
+ try {
+ Object instance = config.createExecutableExtension(ATTR_CLASS);
+ if (instance instanceof SearchParticipant sp) {
+ String languageId = config.getAttribute(ATTR_LANGUAGE_ID);
+ for (String ext : exts) {
+ SearchParticipant existing = this.participantsByExtension.put(ext, sp);
+ if (existing != null && existing != sp) {
+ ILog.get().warn("Duplicate searchParticipant registration for extension '" //$NON-NLS-1$
+ + ext + "': '" + config.getAttribute(ATTR_ID) //$NON-NLS-1$
+ + "' overrides previous participant"); //$NON-NLS-1$
+ }
+ String langId = languageId != null && !languageId.isBlank()
+ ? languageId.trim() : ext;
+ this.languageIdsByExtension.put(ext, langId);
+ }
+ } else {
+ ILog.get().error("searchParticipant '" + config.getAttribute(ATTR_ID) //$NON-NLS-1$
+ + "' class does not extend SearchParticipant: " //$NON-NLS-1$
+ + config.getAttribute(ATTR_CLASS));
+ }
+ } catch (CoreException e) {
+ ILog.get().error("Could not instantiate searchParticipant: '" //$NON-NLS-1$
+ + config.getAttribute(ATTR_ID) + "'", e); //$NON-NLS-1$
+ }
+ }
+ }
+
+ private synchronized void reload() {
+ this.participantsByExtension.clear();
+ this.languageIdsByExtension.clear();
+ load();
+ }
+
+ /**
+ * Disposes this registry by removing its extension registry listener.
+ *
+ * This should be called during JavaCore/JavaModelManager shutdown to
+ * avoid the extension registry retaining references to this classloader
+ * after the {@code org.eclipse.jdt.core} bundle is stopped or updated.
+ */
+ public void dispose() {
+ Platform.getExtensionRegistry().removeListener(this);
+ }
+
+ /**
+ * Disposes the singleton instance of this registry.
+ *
+ * Intended to be invoked from JavaCore/JavaModelManager shutdown.
+ */
+ public static void disposeInstance() {
+ INSTANCE.dispose();
+ }
+
+ /**
+ * Returns the search participant registered for the given file extension,
+ * or {@code null} if none.
+ *
+ * @param fileExtension file extension without dot, e.g. {@code "kt"}
+ * @return the search participant, or {@code null}
+ */
+ public static SearchParticipant getParticipant(String fileExtension) {
+ if (fileExtension == null) return null;
+ synchronized (INSTANCE) {
+ return INSTANCE.participantsByExtension.get(fileExtension.toLowerCase());
+ }
+ }
+
+ /**
+ * Returns all contributed search participants. Does not include the default
+ * Java search participant.
+ *
+ * @return array of contributed participants (may be empty, never null)
+ */
+ public static SearchParticipant[] getContributedParticipants() {
+ synchronized (INSTANCE) {
+ Set unique = new LinkedHashSet<>(INSTANCE.participantsByExtension.values());
+ return unique.toArray(new SearchParticipant[0]);
+ }
+ }
+
+ /**
+ * Returns the LSP language identifier for the given file extension,
+ * or {@code null} if no search participant is registered for it.
+ *
+ * If the participant's extension point registration includes a
+ * {@code languageId} attribute, that value is returned. Otherwise,
+ * the file extension itself is used as the language identifier.
+ *
+ * @param fileExtension file extension without dot, e.g. {@code "kt"}
+ * @return the language identifier (e.g. {@code "kotlin"}), or {@code null}
+ */
+ public static String getLanguageId(String fileExtension) {
+ if (fileExtension == null) return null;
+ synchronized (INSTANCE) {
+ return INSTANCE.languageIdsByExtension.get(fileExtension.toLowerCase());
+ }
+ }
+
+ /**
+ * Returns whether the given file extension has a registered search participant.
+ *
+ * @param fileExtension file extension without dot
+ * @return true if a participant is registered for this extension
+ */
+ public static boolean hasParticipant(String fileExtension) {
+ if (fileExtension == null) return false;
+ synchronized (INSTANCE) {
+ return INSTANCE.participantsByExtension.containsKey(fileExtension.toLowerCase());
+ }
+ }
+
+ /**
+ * Resets the registry, forcing a fresh reload from the extension registry.
+ * Intended for testing only.
+ */
+ public static void reset() {
+ INSTANCE.reload();
+ }
+
+ /**
+ * Returns the file extension from a file name, or {@code null} if none.
+ *
+ * @param fileName the file name (e.g. {@code "Foo.kt"})
+ * @return the extension without dot (e.g. {@code "kt"}), or {@code null}
+ */
+ public static String getFileExtension(String fileName) {
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex < 0) return null;
+ return fileName.substring(dotIndex + 1);
+ }
+
+ @Override
+ public void added(IExtension[] extensions) {
+ reload();
+ }
+
+ @Override
+ public void removed(IExtension[] extensions) {
+ reload();
+ }
+
+ @Override
+ public void added(IExtensionPoint[] extensionPoints) {
+ // not applicable — we listen for extension additions, not extension point additions
+ }
+
+ @Override
+ public void removed(IExtensionPoint[] extensionPoints) {
+ // not applicable
+ }
+}
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/MatchLocator.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/MatchLocator.java
index 219f27f4914..143fe34314f 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/MatchLocator.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/MatchLocator.java
@@ -333,8 +333,12 @@ public MatchLocator(
}
if (pattern instanceof MethodPattern) {
IType type = ((MethodPattern) pattern).declaringType;
- if (type != null && !type.isBinary()) {
- SourceType sourceType = (SourceType) type;
+ // Guard with instanceof: contributed search participants
+ // (e.g., Kotlin) may provide IType implementations that are
+ // non-binary but not SourceType. The local-class statement
+ // retention optimization only applies to Java source types.
+ if (type != null && !type.isBinary()
+ && type instanceof SourceType sourceType) {
IMember local = sourceType.getOuterMostLocalContext();
if (local instanceof IMethod) { // remember this method's range so we don't purge its statements.
try {
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/OrPattern.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/OrPattern.java
index 2794ef01923..4ae7382d19f 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/OrPattern.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/OrPattern.java
@@ -89,6 +89,15 @@ public SearchPattern getBlankPattern() {
return null;
}
+ /**
+ * Returns the sub-patterns combined by this OR pattern.
+ *
+ * @return the component patterns, never {@code null}
+ */
+ public SearchPattern[] getPatterns() {
+ return this.patterns;
+ }
+
boolean isErasureMatch() {
return (this.matchCompatibility & R_ERASURE_MATCH) != 0;
}
diff --git a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/SuperTypeNamesCollector.java b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/SuperTypeNamesCollector.java
index e5a72981121..82a0656ba4b 100644
--- a/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/SuperTypeNamesCollector.java
+++ b/org.eclipse.jdt.core/search/org/eclipse/jdt/internal/core/search/matching/SuperTypeNamesCollector.java
@@ -193,9 +193,12 @@ public char[][][] collect() throws JavaModelException {
BinaryTypeBinding binding = this.locator.cacheBinaryType(this.type, null);
if (binding != null)
collectSuperTypeNames(binding, null);
- } else {
+ // Guard with instanceof: contributed search participants
+ // (e.g., Kotlin) may provide IType implementations that are
+ // non-binary but not SourceType. The supertype collection
+ // via Java AST parsing only applies to Java source types.
+ } else if (this.type instanceof SourceType sourceType) {
ICompilationUnit unit = this.type.getCompilationUnit();
- SourceType sourceType = (SourceType) this.type;
boolean isTopLevelOrMember = sourceType.getOuterMostLocalContext() == null;
CompilationUnitDeclaration parsedUnit = buildBindings(unit, isTopLevelOrMember);
if (parsedUnit != null) {