From 89c7ae7d4cc037c386d982623754a85c8f297030 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:34:05 +0800 Subject: [PATCH] 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 --- crates/ide-host/src/ops/lsp.rs | 4 ++-- crates/ide-lsp/src/lib.rs | 9 ++++++--- crates/ide-lsp/tests/mock_server.rs | 26 ++++++++++++++++++++++++++ languages/java.toml | 3 +++ src/editor/lsp/codeLens.ts | 16 ++++++++++------ 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/crates/ide-host/src/ops/lsp.rs b/crates/ide-host/src/ops/lsp.rs index e3ca31a0a..8c37732e9 100644 --- a/crates/ide-host/src/ops/lsp.rs +++ b/crates/ide-host/src/ops/lsp.rs @@ -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)] diff --git a/crates/ide-lsp/src/lib.rs b/crates/ide-lsp/src/lib.rs index e3a60bd34..6910ff74a 100644 --- a/crates/ide-lsp/src/lib.rs +++ b/crates/ide-lsp/src/lib.rs @@ -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) }; diff --git a/crates/ide-lsp/tests/mock_server.rs b/crates/ide-lsp/tests/mock_server.rs index fe332d9b1..68cd773ec 100644 --- a/crates/ide-lsp/tests/mock_server.rs +++ b/crates/ide-lsp/tests/mock_server.rs @@ -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")] diff --git a/languages/java.toml b/languages/java.toml index e4d60f864..404db1cc9 100644 --- a/languages/java.toml +++ b/languages/java.toml @@ -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" } diff --git a/src/editor/lsp/codeLens.ts b/src/editor/lsp/codeLens.ts index 34e7b1c5b..fc2f38dd4 100644 --- a/src/editor/lsp/codeLens.ts +++ b/src/editor/lsp/codeLens.ts @@ -35,7 +35,7 @@ const LENS_KINDS = new Set([ 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(); - private incomplete = false; + /** Keys whose count came back incomplete (the server was indexing): not shown, asked again once it settles. */ + private incomplete = new Set(); 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) {