From 7965d171f0aae175bc68de402bb4daef6a30d648 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 7 Nov 2026 10:12:26 +0800 Subject: [PATCH] perf(java): keep background usage counts and implementation markers out of the dependencies (#1900) Usage-count lenses and Safe Delete now search only the workspace's sources, and a source type with no implementations there is answered without building the type hierarchy over every dependency jar. These background searches had piled up in jdtls and delayed Show Usages and navigation by seconds in projects with many dependencies. --- crates/ide-lsp/src/lib.rs | 21 +++++++++++++++------ crates/ide-lsp/tests/mock_server.rs | 22 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/crates/ide-lsp/src/lib.rs b/crates/ide-lsp/src/lib.rs index 6390eb679..0f7246cda 100644 --- a/crates/ide-lsp/src/lib.rs +++ b/crates/ide-lsp/src/lib.rs @@ -1364,8 +1364,14 @@ impl LspManager { let mut first = Vec::new(); if method == "textDocument/implementation" { let (locs, complete) = self.java_source_implementations(snap, t, pos, cancel).await; - if complete && !locs.is_empty() { - return Ok(locs); + // A type declared in the sources has no implementations in the dependencies: + // none found there is the whole answer, without a type hierarchy over every jar. + if complete { + if !locs.is_empty() { + return Ok(locs); + } + out.value = Some(Vec::new()); + continue; } if let Some(partial) = partial && !locs.is_empty() @@ -1423,9 +1429,9 @@ 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 - /// there arrive on `partial` before the dependencies and the JDK are searched. + /// [`Self::references`]. Java: without `libraries`, only the workspace's sources are + /// searched (`libraries_skipped` when `project_wide`); 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( &self, @@ -1578,7 +1584,10 @@ impl LspManager { }; scope::Widened { first: all, second: Some(found), incomplete: sources.incomplete } } else { - let first = query(false).await?; + // Bulk probes (usage counts, Safe Delete) ask about the sources' own symbols, + // which no dependency uses: searching every jar and the JDK only adds seconds + // per symbol and delays the searches the user asks for. + 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 2eb71d768..f6275bb32 100644 --- a/crates/ide-lsp/tests/mock_server.rs +++ b/crates/ide-lsp/tests/mock_server.rs @@ -1836,10 +1836,11 @@ async fn java_implementations_come_from_sources_first() { .collect::() }; *s.responder.lock() = Some(Arc::new(move |_, method, params| match method { - // Line 0 is declared in the sources; line 2 in a dependency, which implements it on line 3. + // Lines 0 and 1 are declared in the sources; 2 and 3 in a dependency, which implements 2 on line 3. "workspace/executeCommand" => Some(match params["arguments"][1].as_u64() { Some(0) => json!({ "locations": at(&[1]), "complete": true }), Some(2) => json!({ "locations": at(&[1]), "complete": false }), + Some(3) => json!({ "locations": [], "complete": false }), _ => json!({ "locations": [], "complete": true }), }), "textDocument/implementation" => Some(at(&[3, 1])), @@ -1870,7 +1871,14 @@ async fn java_implementations_come_from_sources_first() { assert_eq!(lines(&found), vec![3, 1]); assert_eq!(s.requested("textDocument/implementation").len(), 2); - // None in the sources: the standard request, with the dependencies. + // Declared in the sources and implemented nowhere there: no search through the dependencies. + let found = h.mgr.goto(doc, Pos::new(1, 6), GotoKind::Implementation).await.unwrap(); + assert!(found.is_empty()); + let markers = h.mgr.implementations(doc, &[Pos::new(1, 6)], Default::default()).await.unwrap(); + assert!(markers[0].is_empty()); + assert_eq!(s.requested("textDocument/implementation").len(), 2); + + // None in the sources of a dependency's type: the standard request, with the dependencies. let partial = Mutex::new(Vec::new()); let found = h .mgr @@ -3100,9 +3108,17 @@ async fn java_usages_in_the_sources_arrive_before_the_dependencies() { let scopes: Vec<_> = s.requested("workspace/executeCommand")[3..].iter().map(|p| p["arguments"].get(4).cloned()).collect(); assert_eq!(scopes, [Some(json!("sources")), Some(json!("sources"))]); - // Scoped probes (code lenses) search everything at once. + // Scoped probes with the libraries search everything at once. let scoped = h.mgr.references(doc, Pos::new(0, 5), true, false, Default::default()).await.unwrap(); assert_eq!(scoped.locations.len(), 2); + // Usage counts and Safe Delete (no libraries): the sources alone, not widened. + let scoped = + h.mgr.references_streaming(doc, Pos::new(0, 5), true, false, false, Default::default(), None).await.unwrap(); + assert_eq!(scoped.locations.len(), 1); + assert!(scoped.locations.iter().all(|l| l.uri.is_none())); + let scopes: Vec<_> = + s.requested("workspace/executeCommand")[5..].iter().map(|p| p["arguments"].get(4).cloned()).collect(); + assert_eq!(scopes, [None, Some(json!("sources"))]); } #[tokio::test(flavor = "multi_thread")]