perf(java): count usage lenses in the project sources only (#2244)

* perf(java): count usage lenses in the project sources only

Usage lenses asked JDT LS for a full references search per declaration,
which also scans every dependency jar and the JDK. In a library class such
as java.lang.String that kept JDT LS busy for tens of seconds per pass,
and failed counts were asked again on every scroll.

- Scoped Java searches without libraries pass the "sources" scope.
- Lenses count declarations near the viewport (10 lines instead of 60).
- Incomplete counts are remembered per document version and asked again
  once the server settles, instead of on every pass.

* perf(java): turn off the language server's own reference lenses

JDT LS shows a references lens above each declaration by default and the
editor resolved them all at once, a references search per declaration on
every open and edit. The editor's usage lenses already count usages.

* style(editor): keep the usage lens's unrelated lines as they were
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-09 14:34:05 +08:00
1 джерело 89b178f9aa
коміт 89c7ae7d4c
5 files changed
+47 -11

No files matched your search

+2 -2
Переглянути файл
@@ -592,8 +592,8 @@ pub async fn lsp_resolve_class_file(host: &Host, window: &str, path: std::path::
/// References of the symbol at `pos`. `project_wide` also searches project files
/// outside the language server's analysis scope; bulk scans over many symbols
/// leave it off. Java, `project_wide`: without `libraries`, the dependencies and the JDK
/// are not searched; with it, those found in the sources arrive on `on_partial` before
/// leave it off. Java: without `libraries`, the dependencies and the JDK are not
/// searched; with it and `project_wide`, those found in the sources arrive on `on_partial` before
/// the slower search for the rest, and the result has them all. Cancel with
/// `cancel_request(request)`.
#[allow(clippy::too_many_arguments)]
+6 -3
Переглянути файл
@@ -1462,8 +1462,8 @@ impl LspManager {
self.references_streaming(doc, pos, include_declaration, project_wide, true, cancel, None).await
}
/// [`Self::references`]. Java, `project_wide`: without `libraries`, only the
/// workspace's sources are searched (`libraries_skipped`); with it, those found
/// [`Self::references`]. Java: without `libraries`, only the workspace's sources
/// are searched (`libraries_skipped`); with it and `project_wide`, those found
/// there arrive on `partial` before the dependencies and the JDK are searched.
#[allow(clippy::too_many_arguments)]
pub async fn references_streaming(
@@ -1630,7 +1630,10 @@ impl LspManager {
};
scope::Widened { first: all, second: Some(found), incomplete: sources.incomplete }
} else {
let first = query(false).await?;
// Scoped probes (usage counts, bulk scans) run for many symbols at once:
// without `libraries`, the dependencies and the JDK are not searched.
libraries_skipped = !libraries;
let first = query(!libraries).await?;
let empty = first.is_empty();
scope::Widened::only(first, empty, &t.server)
};
+26
Переглянути файл
@@ -3752,6 +3752,32 @@ async fn classified_java_references_widen_without_losing_metadata() {
}
}
/// Usage counts probe many symbols at once: without `libraries` they search the sources alone.
#[tokio::test(flavor = "multi_thread")]
async fn scoped_java_usages_search_the_sources_unless_libraries_are_asked_for() {
let mut caps = incremental_caps();
caps["executeCommandProvider"] = json!({"commands": ["openidea.java.usages"]});
let h = harness_for("java", "java", &[("jdtls", caps)], &[]);
let s = h.launcher.state("jdtls");
s.respond("workspace/executeCommand", json!([]));
let (doc, _) = h.open("a.java", "int target;");
h.wait_status("jdtls", ServerStatus::Ready).await;
let seen = grep_candidates(&h);
for libraries in [false, true] {
let refs = h
.mgr
.references_streaming(doc, Pos::new(0, 5), false, false, libraries, Default::default(), None)
.await
.unwrap();
assert_eq!(refs.libraries_skipped, !libraries);
let requested = s.requested("workspace/executeCommand");
let arguments = requested.last().unwrap()["arguments"].as_array().unwrap().clone();
assert_eq!(arguments.get(4) == Some(&json!("sources")), !libraries);
assert_eq!(arguments.len(), if libraries { 4 } else { 5 });
}
assert!(seen.lock().is_empty());
}
/// A library method's usages in the sources are shown before the dependencies and the JDK
/// are searched, which takes seconds; scope widening searches the sources alone again.
#[tokio::test(flavor = "multi_thread")]
+3
Переглянути файл
@@ -43,6 +43,9 @@ contentProvider = { preferred = "fernflowerContentProvider" }
maven = { downloadSources = true, updateSnapshots = false }
eclipse = { downloadSources = true }
references = { includeDecompiledSources = true }
# The editor's usage lenses count usages; the server's own lenses would search every
# declaration of the file again, all at once (on by default in JDT LS).
referencesCodeLens = { enabled = false }
signatureHelp = { enabled = true }
symbols = { includeGeneratedCode = true }
configuration = { updateBuildConfiguration = "automatic" }
+10 -6
Переглянути файл
@@ -35,7 +35,7 @@ const LENS_KINDS = new Set<SymbolKind>([
const MAX_PER_PASS = 40;
const CONCURRENCY = 4;
/** Lines above/below the viewport that are counted too, so scrolling feels instant. */
const MARGIN_LINES = 60;
const MARGIN_LINES = 10;
const usagesBackoff = featureBackoff();
export interface LensTarget {
@@ -114,7 +114,8 @@ class UsagesLensPlugin {
private outline: { doc: Text; targets: LensTarget[] } | null = null;
private generation = 0;
private pending = new Set<RequestId>();
private incomplete = false;
/** Keys whose count came back incomplete (the server was indexing): not shown, asked again once it settles. */
private incomplete = new Set<string>();
private unsubscribe: () => void;
private offReady: () => void;
private offSettled: () => void;
@@ -129,10 +130,13 @@ class UsagesLensPlugin {
// Counts computed while the server was indexing may be incomplete.
this.offReady = onLspRefresh("codeLens", () => {
this.cache.clear();
this.incomplete.clear();
this.refresh();
});
this.offSettled = onServerSettled(() => {
if (this.incomplete) this.refresh();
if (!this.incomplete.size) return;
this.incomplete.clear();
this.refresh();
});
this.offBlame = onBlameInvalidated(() => this.refresh());
}
@@ -167,11 +171,11 @@ class UsagesLensPlugin {
}
const text = view.state.doc;
const wanted = () => gen === this.generation && view.state.doc === text;
this.incomplete = false;
// Blame runs beside the reference requests; it never holds up the counts.
const blame = authors ? blameFor(doc.path, text) : Promise.resolve(null);
if (this.cacheDoc !== text) {
this.cache = new Map();
this.incomplete = new Set();
this.cacheDoc = text;
}
try {
@@ -187,7 +191,7 @@ class UsagesLensPlugin {
.filter((t) => t.line >= firstLine && t.line <= lastLine)
.slice(0, MAX_PER_PASS);
const key = (t: LensTarget) => `${t.line}:${t.col}:${t.name}`;
const todo = usages ? visible.filter((t) => !this.cache.has(key(t))) : [];
const todo = usages ? visible.filter((t) => !this.cache.has(key(t)) && !this.incomplete.has(key(t))) : [];
let unavailable = false;
for (let i = 0; i < todo.length && !unavailable; i += CONCURRENCY) {
await Promise.all(
@@ -209,7 +213,7 @@ class UsagesLensPlugin {
);
}, wanted);
if (wanted()) {
if (incomplete) this.incomplete = true;
if (incomplete) this.incomplete.add(key(t));
else this.cache.set(key(t), refs.length);
}
} catch (e) {