fix(lsp): skip widening for indexed source files (#2052)

Fixes #2034

Call hierarchy, Find Usages and rename widen a server's answer by opening project files that mention the symbol's name and asking again. For JDT LS those files are mostly sources it already analyses, so nearly every incoming-calls request ran a second, slow round before anything was shown. Widening now asks the server which candidates are already in its analysis scope (optional `openidea.workspace.coveredFiles` command, served by the bundled JDT extension using the live source search scope) and only opens the rest; a missing or failing answer keeps the previous behaviour. Covers call hierarchy, references and rename. Hierarchy on a multi-module Maven fixture: median 703 ms → 224 ms, one query round.
This commit is contained in:
l0ng-ai authored and GitHub committed 2026-11-08 03:53:23 +08:00
1 parent af19880390
commit 292ddc4d2a
9 files changed
+270 -6

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: OpenIDEA Java usages
Bundle-SymbolicName: ai.openidea.usages;singleton:=true
Bundle-Version: 1.8.0
Bundle-Version: 1.9.0
Bundle-RequiredExecutionEnvironment: JavaSE-21
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime,
+12 -1
View File
@@ -20,6 +20,13 @@ first and runs the full search after it. Rows in library classes (`jdt:` URIs) c
line as `text`, from the contents the client would request for the class, so the client
need not fetch thousands of class files to show them.
`openidea.workspace.coveredFiles([candidateUri, ...])` returns candidate URIs already
covered by the current JDT source search scope. It checks imported compilation units,
classpath inclusion/exclusion rules and test-source policy; it does not infer coverage
from directory names. References, rename and incoming calls leave those files closed
and only widen with the remaining candidates. A missing or failing command retains
widening. The result is query-local, so imports and classpath changes need no cache invalidation.
## Search plan and access semantics
For source fields on the verified JDT LS implementation, a single plan merges
@@ -69,7 +76,7 @@ is not a claim of full IntelliJ feature parity.
## Rebuild and delivery
The checked-in jar is embedded by ide-sdk `include_bytes!`, installed as
`java-usages-1.8.0.jar`, and added alongside existing initialization bundles.
`java-usages-1.9.0.jar`, and added alongside existing initialization bundles.
Users need no compiler, Maven or Python. Manifest/install version must change
when replacing the bundle because OSGi caches bundles.
@@ -108,6 +115,10 @@ configuration index (until the JAR or the project's classpath changes), so a sou
longer resolves every Spring Boot property again; its reproducible SHA-256 is
`728ab33b5e01fa5e496818f25ddbc487458a92339026899b0e22abda5adfd67b`.
Version 1.9.0 adds the analysis-scope command to avoid redundant semantic queries;
its reproducible SHA-256 is
`ae04182d6a86dfdd6f6befaaeb28907fd9e4c022ceedb445f0b32ea917dca379`.
```sh
export PATH=/path/to/jdk25/bin:$PATH
python3 java/java-usages/build.py /path/to/jdtls
Binary file not shown.
+3
View File
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<extension point="org.eclipse.jdt.ls.core.delegateCommandHandler">
<delegateCommandHandler class="ai.openidea.usages.CoveredFilesCommand">
<command id="openidea.workspace.coveredFiles"/>
</delegateCommandHandler>
<delegateCommandHandler class="ai.openidea.usages.UsagesCommand">
<command id="openidea.java.usages"/>
</delegateCommandHandler>
@@ -0,0 +1,43 @@
package ai.openidea.usages;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.*;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager;
import org.eclipse.jdt.ls.core.internal.preferences.Preferences.SearchScope;
/** Candidate files already included in the workspace's semantic source searches. */
public final class CoveredFilesCommand implements IDelegateCommandHandler {
@Override
public Object executeCommand(String command, List<Object> arguments, IProgressMonitor monitor) throws Exception {
var workspace = ResourcesPlugin.getWorkspace().getRoot();
IJavaProject[] projects = JavaCore.create(workspace).getJavaProjects();
var setting = JavaLanguageServerPlugin.getPreferencesManager().getPreferences().getSearchScope();
var scope = SearchEngine.createJavaSearchScope(setting == SearchScope.main, projects,
IJavaSearchScope.SOURCES | IJavaSearchScope.REFERENCED_PROJECTS);
List<String> covered = new ArrayList<>();
for (Object candidate : (List<?>) arguments.get(0)) {
if (monitor.isCanceled()) throw new OperationCanceledException();
String uri = (String) candidate;
for (var file : workspace.findFilesForLocationURI(URI.create(uri))) {
if (!(JavaCore.create(file) instanceof ICompilationUnit unit) || !unit.exists()) continue;
IJavaProject project = unit.getJavaProject();
// The default project holds standalone open files, not an indexed source tree.
if (ProjectsManager.getDefaultProject().equals(project.getProject()) || !project.isOnClasspath(file)) continue;
if (scope.encloses(unit)) {
covered.add(uri);
break;
}
}
}
return covered;
}
}