From d509ed6e816202fb6e2be3b5bb6b7619a98133ca Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:39:00 +0800 Subject: [PATCH] fix(java): stop Show Usages waiting seconds on the configuration index after a change (#2011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usages counts clicked after a Java change waited for the configuration index to resolve every dependency's Spring metadata again (15–20 s in a Spring Boot project). The Java extension (1.8.0) now keeps each dependency JAR's metadata across rebuilds until the JAR or the classpath changes, and Show Usages shows the Java usages first, adding the configuration ones when the index takes longer. Closes #1898 --- crates/ide-lsp/src/lib.rs | 15 +++++- crates/ide-lsp/tests/mock_server.rs | 46 ++++++++++++++++ crates/ide-sdk/src/java.rs | 2 +- java/java-usages/CONFIGURATION.md | 8 ++- java/java-usages/MANIFEST.MF | 2 +- java/java-usages/README.md | 6 ++- java/java-usages/openidea-java-usages.jar | Bin 133533 -> 138464 bytes .../configuration/ConfigurationCommand.java | 51 +++++++++++++++++- java/java-usages/tests/configuration.py | 8 +++ 9 files changed, 131 insertions(+), 7 deletions(-) diff --git a/crates/ide-lsp/src/lib.rs b/crates/ide-lsp/src/lib.rs index d9d6e4b60..4a7d00051 100644 --- a/crates/ide-lsp/src/lib.rs +++ b/crates/ide-lsp/src/lib.rs @@ -1475,7 +1475,20 @@ impl LspManager { }; let native = self.native_references(doc, pos, include_declaration, project_wide, libraries, cancel.clone(), partial); - let (configuration, result) = futures::join!(configuration, native); + futures::pin_mut!(configuration, native); + let (configuration, result) = match futures::future::select(configuration, native).await { + futures::future::Either::Left((configuration, native)) => (configuration, native.await), + futures::future::Either::Right((result, configuration)) => { + // The configuration index is rebuilt after Java changes, which takes seconds in a + // project with many dependencies: the usages found meanwhile are shown first. + if let (Some(partial), Ok(found)) = (partial, &result) + && !found.locations.is_empty() + { + partial(found.clone()); + } + (configuration.await, result) + } + }; let configuration = configuration?; let mut result = result?; if let Some(configuration) = configuration { diff --git a/crates/ide-lsp/tests/mock_server.rs b/crates/ide-lsp/tests/mock_server.rs index 111348dd5..052ba7975 100644 --- a/crates/ide-lsp/tests/mock_server.rs +++ b/crates/ide-lsp/tests/mock_server.rs @@ -4128,6 +4128,52 @@ async fn configuration_usages_merge_native_and_unsaved_profiles_without_sibling_ assert_eq!(refs.incomplete, None); } +/// The configuration index is rebuilt after Java changes, which can take seconds: Show Usages +/// shows the Java usages meanwhile, then adds the configuration ones. +#[tokio::test(flavor = "multi_thread")] +async fn java_usages_arrive_before_configuration_usages() { + let mut caps = incremental_caps(); + caps["executeCommandProvider"] = json!({"commands": ["openidea.java.usages", "openidea.java.configuration"]}); + let h = harness_for("java", "java", &[("jdtls", caps)], &[]); + let s = h.launcher.state("jdtls"); + let yaml = h.root.join("application.yml"); + std::fs::write(&yaml, "app:\n target: 1\n").unwrap(); + let property = json!({"uri": uri_of(&yaml), "range": { + "start": {"line": 1, "character": 2}, "end": {"line": 1, "character": 8}}}); + let gate = Arc::new(Gate::default()); + let held = gate.clone(); + *s.responder.lock() = Some(Arc::new(move |s, method, params| { + (method == "workspace/executeCommand").then(|| { + if params["command"] == "openidea.java.configuration" { + held.hold(); + return json!({"locations": [property.clone()], "keys": [{"name": "app.target", "relaxed": true}], "roots": []}); + } + let docs = s.docs.lock(); + let rows = docs.iter().flat_map(|(uri, text)| occurrences(uri, text)); + Value::Array(rows.map(|location| json!({"location": location, "kind": "unknown"})).collect()) + }) + })); + let (doc, a) = h.open("a.java", "int target;"); + h.wait_status("jdtls", ServerStatus::Ready).await; + eventually(|| s.docs.lock().len() == 1).await; + let partials = Mutex::new(Vec::::new()); + let push = |r| partials.lock().push(r); + let search = h.mgr.references_streaming(doc, Pos::new(0, 5), true, true, false, Default::default(), Some(&push)); + let release = async { + eventually(|| !partials.lock().is_empty()).await; + assert!(gate.entered(), "shown before the configuration lookup started"); + gate.open(); + }; + let (refs, ()) = tokio::join!(search, release); + let refs = refs.unwrap(); + let partials = partials.into_inner(); + assert_eq!(partials.len(), 1); + let paths = |r: &References| r.locations.iter().map(|l| l.path.to_path_buf()).collect::>(); + assert_eq!(paths(&partials[0]), std::slice::from_ref(&a)); + assert_eq!(partials[0].classified.len(), 1); + assert_eq!(paths(&refs), [a, yaml]); +} + #[tokio::test(flavor = "multi_thread")] async fn configuration_extension_failure_preserves_native_java_navigation() { let mut caps = incremental_caps(); diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index 34c70b099..18c513b38 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -1202,7 +1202,7 @@ mod kotlin_bridge_bundle_tests { /// Installs the app-owned JDT LS command bundle, falling back to ordinary LSP on IO failure. pub fn usages_bundle(data_dir: &Path) -> Option { - let extension = data_dir.join("java-usages-1.7.0.jar"); + let extension = data_dir.join("java-usages-1.8.0.jar"); let bytes = include_bytes!("../../../java/java-usages/openidea-java-usages.jar"); if std::fs::read(&extension).is_ok_and(|old| old == bytes) { clean_usages_bundles(data_dir, &extension); diff --git a/java/java-usages/CONFIGURATION.md b/java/java-usages/CONFIGURATION.md index ef12eb1c3..24f49b50f 100644 --- a/java/java-usages/CONFIGURATION.md +++ b/java/java-usages/CONFIGURATION.md @@ -78,7 +78,13 @@ changes; a fingerprint of working copies with unsaved changes covers edits befor they are saved. Opening or closing an editor, reconciling, building, and non-Java resource changes (configuration files are parsed by the client; metadata files are checked by timestamp) keep it. This is **snapshot invalidation**, not a fully -incremental Java semantic index. +incremental Java semantic index. A rebuild reuses each dependency JAR's metadata +associations (up to 100,000 in all) while the JAR's timestamp and size and the project's +classpath are unchanged: resolving a Spring Boot JAR's thousands of properties to library +members takes seconds, and a source change cannot affect them. + +Find Usages shows the Java usages as soon as they are found, then adds the configuration +ones when a rebuild of the snapshot takes longer. In YAML/properties, ⌘/Ctrl-hover underlines the whole key, hyphens and dots included, and doesn't open the documentation popup. Hover and Go to Definition share diff --git a/java/java-usages/MANIFEST.MF b/java/java-usages/MANIFEST.MF index cdcb4bef1..40fc4e42f 100644 --- a/java/java-usages/MANIFEST.MF +++ b/java/java-usages/MANIFEST.MF @@ -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.7.0 +Bundle-Version: 1.8.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, diff --git a/java/java-usages/README.md b/java/java-usages/README.md index 0fde82683..d705841d3 100644 --- a/java/java-usages/README.md +++ b/java/java-usages/README.md @@ -69,7 +69,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.7.0.jar`, and added alongside existing initialization bundles. +`java-usages-1.8.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. @@ -103,6 +103,10 @@ Version 1.7.0 adds the `"sources"` scope to `openidea.java.usages`, searches eac file's decompiled source on its own and returns the line (`text`) of each usage in a library class; its reproducible SHA-256 is `56d35619d5be937b028ac143ba748365a66a74c803bc112e4788a7dce98b44fc`. +Version 1.8.0 keeps each dependency JAR's configuration metadata across rebuilds of the +configuration index (until the JAR or the project's classpath changes), so a source change no +longer resolves every Spring Boot property again; its reproducible SHA-256 is +`728ab33b5e01fa5e496818f25ddbc487458a92339026899b0e22abda5adfd67b`. ```sh export PATH=/path/to/jdk25/bin:$PATH diff --git a/java/java-usages/openidea-java-usages.jar b/java/java-usages/openidea-java-usages.jar index f637e1e40235757b97fb2884eeed95aa98bf04b4..693009b7f6f286e359be3f8573c8eddb6fe8430a 100644 GIT binary patch delta 21099 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 delta 18791 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 { if (affects(event.getDelta())) REVISION.incrementAndGet(); }, ElementChangedEvent.POST_CHANGE); } + /** Entries read from each library's Spring metadata, by project and archive. They depend + * on the archive and the project's classpath only, so a source change rebuilds the index + * without reading every dependency's metadata again: resolving its thousands of properties + * to library members takes seconds. A changed archive stamp or classpath drops them. */ + private static final Map ARCHIVES = new HashMap<>(); + private record Archive(FileStamp stamp, List entries) {} + static { + JavaCore.addElementChangedListener(event -> { + if (affects(event.getDelta())) REVISION.incrementAndGet(); + if (classpathChanged(event.getDelta())) synchronized (ARCHIVES) { ARCHIVES.clear(); } + }, ElementChangedEvent.POST_CHANGE); + } + /** A project or package fragment root added, removed or reconfigured (not a change of + * the sources or resources inside it). */ + static boolean classpathChanged(IJavaElementDelta delta) { + if (delta.getElement().getElementType() > IJavaElement.PACKAGE_FRAGMENT_ROOT) return false; + int flags = delta.getFlags() & ~(IGNORED_FLAGS | IJavaElementDelta.F_CONTENT); + if (delta.getKind() != IJavaElementDelta.CHANGED || flags != 0) return true; + for (IJavaElementDelta child : delta.getAffectedChildren()) if (classpathChanged(child)) return true; + return false; + } + private static List archive(String key, FileStamp stamp) { + synchronized (ARCHIVES) { + Archive known = ARCHIVES.get(key); + return known != null && known.stamp.equals(stamp) ? known.entries : null; + } + } + private static void remember(String key, FileStamp stamp, List entries) { + synchronized (ARCHIVES) { + ARCHIVES.remove(key); + if (ARCHIVES.values().stream().mapToInt(known -> known.entries.size()).sum() + entries.size() > MAX_ENTRIES) + ARCHIVES.clear(); + if (entries.size() <= MAX_ENTRIES) ARCHIVES.put(key, new Archive(stamp, entries)); + } + } static boolean affects(IJavaElementDelta delta) { int flags = delta.getFlags() & ~IGNORED_FLAGS; if (delta.getElement().getElementType() < IJavaElement.COMPILATION_UNIT) flags &= ~IJavaElementDelta.F_CONTENT; @@ -465,8 +499,18 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { Path path = root.getPath().toFile().toPath(); if (!root.isExternal() && root.getResource() != null && root.getResource().getLocation() != null) path = root.getResource().getLocation().toFile().toPath(); - files.put(path, stamp(path)); + FileStamp stamp = stamp(path); + files.put(path, stamp); if (!Files.isRegularFile(path)) return; + String key = project.getHandleIdentifier() + '\n' + path; + List known = archive(key, stamp); + if (known != null && entries.size() + known.size() <= MAX_ENTRIES) { + entries.addAll(known); + return; + } + int start = entries.size(); + boolean before = incomplete; + incomplete = false; try (ZipFile zip = new ZipFile(path.toFile())) { for (String filename : List.of("spring-configuration-metadata.json", "additional-spring-configuration-metadata.json")) { var entry = zip.getEntry("META-INF/" + filename); @@ -481,6 +525,9 @@ public final class ConfigurationCommand implements IDelegateCommandHandler { metadataObject(metadata, project); } } catch (Exception e) { if (e instanceof OperationCanceledException) throw (OperationCanceledException)e; incomplete = true; } + // Only a complete reading is reused; an incomplete one is read again next time. + if (!incomplete) remember(key, stamp, List.copyOf(entries.subList(start, entries.size()))); + incomplete |= before; } void metadataObject(JsonObject metadata, IJavaProject project) throws JavaModelException { JsonArray groups = metadata.has("groups") ? metadata.getAsJsonArray("groups") : new JsonArray(); diff --git a/java/java-usages/tests/configuration.py b/java/java-usages/tests/configuration.py index f1e09745b..edcddba66 100644 --- a/java/java-usages/tests/configuration.py +++ b/java/java-usages/tests/configuration.py @@ -244,6 +244,14 @@ try: while not query('added.size')['locations']: assert time.monotonic() < deadline, 'a new source on disk did not reach the index' time.sleep(0.2) + # That rebuild reused the library's metadata; a changed archive is read again. + assert query('vendor.port')['locations'], 'a source rebuild lost the library metadata' + metadata['properties'].append({'name': 'vendor.speed', 'type': 'java.lang.Integer', 'sourceType': 'vendor.LibrarySettings'}) + with zipfile.ZipFile(library, 'w') as z: + z.write(classes / 'vendor/LibrarySettings.class', 'vendor/LibrarySettings.class') + z.writestr('META-INF/spring-configuration-metadata.json', json.dumps(metadata)) + assert query('vendor.speed')['locations'], 'a changed library archive kept its old metadata' + assert query('vendor.port')['locations'] # Unsaved changes must replace old keys, and closing must restore disk keys. changed = source.replace('prefix="app"', 'prefix="edited"').replace('${app.time-out:30}', '${edited.timeout:30}') send({'jsonrpc': '2.0', 'method': 'textDocument/didChange', 'params': {'textDocument': {'uri': file.as_uri(), 'version': 2}, 'contentChanges': [{'text': changed}]}})