mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-09 17:20:38 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0717358df |
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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::<References>::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::<Vec<_>>();
|
||||
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();
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -41,7 +41,41 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
* own stamps), do not change the index either. */
|
||||
private static final int IGNORED_FLAGS = IJavaElementDelta.F_CHILDREN | IJavaElementDelta.F_PRIMARY_WORKING_COPY
|
||||
| IJavaElementDelta.F_AST_AFFECTED | IJavaElementDelta.F_FINE_GRAINED;
|
||||
static { JavaCore.addElementChangedListener(event -> { 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<String, Archive> ARCHIVES = new HashMap<>();
|
||||
private record Archive(FileStamp stamp, List<Entry> 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<Entry> 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<Entry> 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<Entry> 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();
|
||||
|
||||
@@ -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}]}})
|
||||
|
||||
Reference in new issue
Block a user