Skip to content

Commit 57b2ef4

Browse files
committed
Add searchParticipant extension point for javaDerivedSource indexing
Extend JDT Core's indexing pipeline to discover and index files registered under the javaDerivedSource content type (e.g. .kt, .scala, .groovy) via contributed SearchParticipant implementations. Changes: - New org.eclipse.jdt.core.searchParticipant extension point with schema - SearchParticipantRegistry: loads contributed participants by file extension from the extension registry, listens for dynamic bundle load/unload via IRegistryEventListener to invalidate cached state automatically - IndexManager.addDerivedSource(): routes derived files to their registered participant instead of the default JavaSearchParticipant - IndexAllProject: discovers javaDerivedSource files alongside Java files in source folders and indexes them via addDerivedSource() - AddFolderToIndex: same derived file discovery for folder additions - DeltaProcessor: handles add/change/remove deltas for derived source files - SearchEngine.getSearchParticipants(): new API returning the default participant plus all contributed participants - SearchParticipant.locateCallees(): new default method enabling contributed participants to report outgoing call sites for non-Java members, used by the call hierarchy engine when Java AST-based callee analysis is not available - OrPattern.getPatterns(): new public accessor so contributed participants can decompose OrPattern (e.g. REFERENCES + DECLARATIONS) for correct pattern-type dispatch in locateMatches() - Fully backwards-compatible: no registered participants = identical behavior
1 parent 304c65e commit 57b2ef4

27 files changed

Lines changed: 3369 additions & 6 deletions

.claude-tmp/mvn-install.log

Lines changed: 2352 additions & 0 deletions
Large diffs are not rendered by default.

.idea/.gitignore

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/codeStyles/codeStyleConfig.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

org.eclipse.jdt.core.tests.model/plugin.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@
119119
file-extensions="kt">
120120
</file-association>
121121
</extension>
122+
<extension
123+
point="org.eclipse.core.contenttype.contentTypes">
124+
<file-association
125+
content-type="org.eclipse.jdt.core.javaDerivedSource"
126+
file-extensions="langx">
127+
</file-association>
128+
</extension>
129+
<extension
130+
point="org.eclipse.jdt.core.searchParticipant">
131+
<searchParticipant
132+
class="org.eclipse.jdt.core.tests.model.TestDerivedSourceSearchParticipant"
133+
id="org.eclipse.jdt.core.tests.model.testDerivedSourceSearchParticipant"
134+
fileExtensions="langx">
135+
</searchParticipant>
136+
</extension>
122137

123138

124139
</plugin>

org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ private static Class[] getAllTestClasses() {
215215
// Create search participant tests
216216
SearchParticipantTests.class,
217217

218+
// Derived source search participant tests
219+
DerivedSourceSearchParticipantTests.class,
220+
218221
// Class file tests
219222
ClassFileTests.class,
220223

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse Foundation and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Arcadiy Ivanov - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.core.tests.model;
15+
16+
import junit.framework.Test;
17+
import org.eclipse.core.runtime.CoreException;
18+
import org.eclipse.jdt.core.IJavaProject;
19+
import org.eclipse.jdt.core.search.SearchEngine;
20+
import org.eclipse.jdt.core.search.SearchParticipant;
21+
import org.eclipse.jdt.internal.core.search.indexing.SearchParticipantRegistry;
22+
23+
/**
24+
* Tests for the {@code org.eclipse.jdt.core.searchParticipant} extension point
25+
* and the {@link SearchParticipantRegistry}.
26+
*/
27+
public class DerivedSourceSearchParticipantTests extends ModifyingResourceTests {
28+
29+
IJavaProject project;
30+
31+
public DerivedSourceSearchParticipantTests(String name) {
32+
super(name);
33+
}
34+
35+
public static Test suite() {
36+
return buildModelTestSuite(DerivedSourceSearchParticipantTests.class);
37+
}
38+
39+
@Override
40+
protected void setUp() throws Exception {
41+
super.setUp();
42+
TestDerivedSourceSearchParticipant.reset();
43+
SearchParticipantRegistry.reset();
44+
this.project = createJavaProject("DSP", new String[] {"src"}, "bin");
45+
}
46+
47+
@Override
48+
protected void tearDown() throws Exception {
49+
deleteProject("DSP");
50+
this.project = null;
51+
super.tearDown();
52+
}
53+
54+
/**
55+
* Verifies that the registry discovers the {@code .langx} extension
56+
* from the test plugin's contribution.
57+
*/
58+
public void testRegistryHasParticipantForLangx() {
59+
assertTrue("Registry should have participant for langx",
60+
SearchParticipantRegistry.hasParticipant("langx"));
61+
}
62+
63+
/**
64+
* Verifies that the registry does not report a participant for
65+
* an unregistered extension.
66+
*/
67+
public void testRegistryNoParticipantForUnknownExtension() {
68+
assertFalse("Registry should not have participant for unknown_ext",
69+
SearchParticipantRegistry.hasParticipant("unknown_ext"));
70+
}
71+
72+
/**
73+
* Verifies that {@code getParticipant("langx")} returns a non-null
74+
* participant of the correct type and that the instance is reused.
75+
*/
76+
public void testRegistryGetParticipantSingleton() {
77+
SearchParticipant p1 = SearchParticipantRegistry.getParticipant("langx");
78+
assertNotNull("Should return a participant for langx", p1);
79+
assertTrue("Should be a TestDerivedSourceSearchParticipant",
80+
p1 instanceof TestDerivedSourceSearchParticipant);
81+
SearchParticipant p2 = SearchParticipantRegistry.getParticipant("langx");
82+
assertSame("Same instance should be returned on second call", p1, p2);
83+
assertEquals("Exactly one instance should be created",
84+
1, TestDerivedSourceSearchParticipant.instanceCount.get());
85+
}
86+
87+
/**
88+
* Verifies that {@code getContributedParticipants()} includes the
89+
* test participant.
90+
*/
91+
public void testGetContributedParticipants() {
92+
SearchParticipant[] contributed = SearchParticipantRegistry.getContributedParticipants();
93+
assertTrue("Should have at least one contributed participant",
94+
contributed.length >= 1);
95+
boolean found = false;
96+
for (SearchParticipant p : contributed) {
97+
if (p instanceof TestDerivedSourceSearchParticipant) {
98+
found = true;
99+
break;
100+
}
101+
}
102+
assertTrue("Contributed participants should include TestDerivedSourceSearchParticipant",
103+
found);
104+
}
105+
106+
/**
107+
* Verifies that {@link SearchEngine#getSearchParticipants()} returns both
108+
* the default participant and contributed participants.
109+
*/
110+
public void testGetSearchParticipants() {
111+
SearchParticipant[] participants = SearchEngine.getSearchParticipants();
112+
assertTrue("Should have at least 2 participants (default + contributed)",
113+
participants.length >= 2);
114+
boolean hasDefault = false;
115+
boolean hasContributed = false;
116+
SearchParticipant defaultP = SearchEngine.getDefaultSearchParticipant();
117+
for (SearchParticipant p : participants) {
118+
if (p.getClass() == defaultP.getClass()) {
119+
hasDefault = true;
120+
}
121+
if (p instanceof TestDerivedSourceSearchParticipant) {
122+
hasContributed = true;
123+
}
124+
}
125+
assertTrue("Should include the default Java search participant", hasDefault);
126+
assertTrue("Should include the contributed test participant", hasContributed);
127+
}
128+
129+
/**
130+
* Verifies that {@code getFileExtension()} correctly extracts extensions.
131+
*/
132+
public void testGetFileExtension() {
133+
assertEquals("kt", SearchParticipantRegistry.getFileExtension("Foo.kt"));
134+
assertEquals("langx", SearchParticipantRegistry.getFileExtension("Bar.langx"));
135+
assertEquals("java", SearchParticipantRegistry.getFileExtension("Baz.java"));
136+
assertNull(SearchParticipantRegistry.getFileExtension("noextension"));
137+
}
138+
139+
/**
140+
* Verifies that adding a {@code .langx} file to a source folder triggers
141+
* the search participant's {@code indexDocument()} method via the
142+
* automatic indexing pipeline.
143+
*/
144+
public void testIndexingTriggeredForDerivedSourceFile() throws CoreException {
145+
createFile(
146+
"/DSP/src/Hello.langx",
147+
"public class Hello {\n" +
148+
" public void greet() {}\n" +
149+
"}"
150+
);
151+
waitUntilIndexesReady();
152+
assertTrue("indexDocument should have been called at least once",
153+
TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() > 0);
154+
}
155+
156+
/**
157+
* Verifies that adding a second {@code .langx} file triggers additional
158+
* indexing calls.
159+
*/
160+
public void testIndexingTriggeredForMultipleDerivedSourceFiles() throws CoreException {
161+
createFile(
162+
"/DSP/src/Alpha.langx",
163+
"public class Alpha {\n" +
164+
" public int value() { return 1; }\n" +
165+
"}"
166+
);
167+
createFile(
168+
"/DSP/src/Beta.langx",
169+
"public class Beta {\n" +
170+
" public int value() { return 2; }\n" +
171+
"}"
172+
);
173+
waitUntilIndexesReady();
174+
assertTrue("indexDocument should have been called at least twice",
175+
TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() >= 2);
176+
}
177+
178+
/**
179+
* Verifies that creating a {@code .langx} file in a subfolder/package
180+
* triggers indexing via the add-folder-to-index path.
181+
*/
182+
public void testIndexingInSubPackage() throws CoreException {
183+
createFolder("/DSP/src/pkg");
184+
createFile(
185+
"/DSP/src/pkg/InPackage.langx",
186+
"package pkg;\n" +
187+
"public class InPackage {}"
188+
);
189+
waitUntilIndexesReady();
190+
assertTrue("indexDocument should have been called for file in subpackage",
191+
TestDerivedSourceSearchParticipant.indexDocumentCallCount.get() > 0);
192+
}
193+
194+
/**
195+
* Verifies that deleting a {@code .langx} file does not crash
196+
* and that the index is updated.
197+
*/
198+
public void testDeletionOfDerivedSourceFile() throws CoreException {
199+
createFile(
200+
"/DSP/src/ToDelete.langx",
201+
"public class ToDelete {}"
202+
);
203+
waitUntilIndexesReady();
204+
205+
// delete the file — should not throw
206+
deleteFile("/DSP/src/ToDelete.langx");
207+
waitUntilIndexesReady();
208+
// If we get here without an exception, the delta processor handled removal correctly
209+
}
210+
211+
/**
212+
* Verifies that regular {@code .java} files are not routed to the
213+
* derived source participant.
214+
*/
215+
public void testJavaFilesNotRoutedToParticipant() throws CoreException {
216+
createFile(
217+
"/DSP/src/Regular.java",
218+
"public class Regular {}"
219+
);
220+
waitUntilIndexesReady();
221+
assertEquals("indexDocument should not be called for .java files",
222+
0, TestDerivedSourceSearchParticipant.indexDocumentCallCount.get());
223+
}
224+
225+
/**
226+
* Verifies that the registry reset clears cached participants
227+
* and forces re-loading on next access.
228+
*/
229+
public void testRegistryReset() {
230+
SearchParticipant before = SearchParticipantRegistry.getParticipant("langx");
231+
assertNotNull(before);
232+
TestDerivedSourceSearchParticipant.reset();
233+
SearchParticipantRegistry.reset();
234+
235+
SearchParticipant after = SearchParticipantRegistry.getParticipant("langx");
236+
assertNotNull(after);
237+
assertNotSame("After reset, a new instance should be created", before, after);
238+
assertEquals("New instance should have been created after reset",
239+
1, TestDerivedSourceSearchParticipant.instanceCount.get());
240+
}
241+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse Foundation and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Arcadiy Ivanov - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.core.tests.model;
15+
16+
import org.eclipse.core.resources.IFile;
17+
import org.eclipse.core.resources.ResourcesPlugin;
18+
import org.eclipse.core.runtime.CoreException;
19+
import org.eclipse.core.runtime.Path;
20+
import org.eclipse.jdt.core.JavaModelException;
21+
import org.eclipse.jdt.core.search.SearchDocument;
22+
import org.eclipse.jdt.core.search.SearchParticipant;
23+
import org.eclipse.jdt.internal.core.util.Util;
24+
25+
/**
26+
* A search document for derived source files (e.g. {@code .langx}).
27+
* Reads file contents from the workspace.
28+
*/
29+
public class TestDerivedSearchDocument extends SearchDocument {
30+
31+
private IFile file;
32+
33+
public TestDerivedSearchDocument(String documentPath, SearchParticipant participant) {
34+
super(documentPath, participant);
35+
}
36+
37+
@Override
38+
public byte[] getByteContents() {
39+
try {
40+
return Util.getResourceContentsAsByteArray(getFile());
41+
} catch (JavaModelException e) {
42+
return null;
43+
}
44+
}
45+
46+
@Override
47+
public char[] getCharContents() {
48+
try {
49+
return Util.getResourceContentsAsCharArray(getFile());
50+
} catch (JavaModelException e) {
51+
return null;
52+
}
53+
}
54+
55+
@Override
56+
public String getEncoding() {
57+
IFile resource = getFile();
58+
if (resource != null) {
59+
try {
60+
return resource.getCharset();
61+
} catch (CoreException e) {
62+
// fall through
63+
}
64+
}
65+
return null;
66+
}
67+
68+
private IFile getFile() {
69+
if (this.file == null) {
70+
this.file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(getPath()));
71+
}
72+
return this.file;
73+
}
74+
}

0 commit comments

Comments
 (0)