From 5ad179a9e6d6a2ac65d3a7d9d82bcb11634e7a43 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:12:43 +0800 Subject: [PATCH] perf(java): stop configuration indexing from blocking navigation and keep it warm (#1012) Java Go to Definition consults the Spring configuration command only inside @Value / getProperty / getRequiredProperty arguments (tree-sitter check), so ordinary Ctrl+click and Ctrl+hover go straight to JDT instead of waiting on a project-wide index rebuild. The java-usages bundle (1.2.1) keeps its configuration snapshot across editor open/close, builds, saves of non-Java resources and reconciles; it invalidates on Java model changes and unsaved Java edits only. Fixes #998 Fixes #1002 --- .../ide-language/src/syntax/configuration.rs | 82 +++++++++++++++++- crates/ide-language/src/syntax/lines.rs | 10 +++ crates/ide-language/src/syntax/web.rs | 11 +-- crates/ide-lsp/src/jdtls/configuration.rs | 16 +++- crates/ide-lsp/tests/mock_server.rs | 35 +++++++- crates/ide-sdk/src/java.rs | 2 +- plugins/java-usages/CONFIGURATION.md | 13 +-- plugins/java-usages/MANIFEST.MF | 2 +- plugins/java-usages/README.md | 5 +- plugins/java-usages/openidea-java-usages.jar | Bin 82033 -> 83078 bytes .../configuration/ConfigurationCommand.java | 27 ++++-- plugins/java-usages/tests/configuration.py | 29 ++++++- 12 files changed, 203 insertions(+), 29 deletions(-) diff --git a/crates/ide-language/src/syntax/configuration.rs b/crates/ide-language/src/syntax/configuration.rs index 43c1cb5cf..4a1a081f0 100644 --- a/crates/ide-language/src/syntax/configuration.rs +++ b/crates/ide-language/src/syntax/configuration.rs @@ -1,9 +1,9 @@ //! Source ranges for Spring-style configuration keys. This is a syntax index, //! not a YAML evaluator: aliases, merge keys and runtime profile precedence are //! intentionally left to the YAML server / application. -use super::lines::LineIndex; +use super::lines::{LineIndex, byte_at}; use super::{ParsedTree, Syntax, TreeError}; -use ide_core::{LanguageId, Range}; +use ide_core::{LanguageId, Pos, Range}; use tree_sitter::Node; #[derive(Clone, Debug, PartialEq, Eq)] @@ -26,6 +26,40 @@ impl Syntax { } } +/// Spring's property lookups whose first argument names a configuration key. +const PROPERTY_LOOKUPS: [&str; 2] = ["getProperty", "getRequiredProperty"]; + +impl Syntax { + /// Whether `pos` in Java `text` lies where a configuration key can be written: + /// inside the arguments of a `@Value` annotation or of a `getProperty` / + /// `getRequiredProperty` call. Everywhere else Go to Definition is ordinary + /// Java navigation and need not consult the configuration index. A file the + /// grammar cannot parse answers false. + pub fn java_configuration_site(&self, text: &str, pos: Pos, cancelled: &dyn Fn() -> bool) -> bool { + let Some(at) = byte_at(text, pos) else { return false }; + self.with_parsed(&LanguageId::new("java"), text, cancelled, |tree| { + let mut node = tree.tree.root_node().descendant_for_byte_range(at, at); + while let Some(n) = node { + let Some(parent) = n.parent() else { break }; + let callee = parent.child_by_field_name("name").map(|name| &text[name.byte_range()]); + match (n.kind(), parent.kind(), callee) { + ("annotation_argument_list", "annotation", Some(name)) => { + return name.rsplit('.').next() == Some("Value"); + } + ("argument_list", "method_invocation", Some(name)) if PROPERTY_LOOKUPS.contains(&name) => { + return true; + } + // Keys are expressions; nothing above a body or declaration can hold one. + (_, "block" | "class_body" | "interface_body" | "program", _) => return false, + _ => node = Some(parent), + } + } + false + }) + .unwrap_or(false) + } +} + impl ParsedTree<'_> { pub fn configuration_keys(&self, cancelled: &dyn Fn() -> bool) -> Vec { let mut out = Vec::new(); @@ -188,6 +222,50 @@ fn unescape_property(raw: &str) -> Option { #[cfg(test)] mod tests { use super::*; + + #[test] + fn java_configuration_sites() { + let text = r#"package a; +import org.springframework.beans.factory.annotation.Value; +class A { + @Value("${app.port:80}") int port; + @org.springframework.beans.factory.annotation.Value(value = Keys.NAME) String name; + @Values("x") int other; + @Override public String toString() { + String v = env.getProperty("app.host", "h") + env.getRequiredProperty(Keys.USER); + return helper.getValue("😀 app.port") + String.valueOf(port) + env.getProperty("k").trim(); + } +} +"#; + let syntax = Syntax::new(); + let at = |needle: &str, offset: u32| { + let line = text.lines().position(|l| l.contains(needle)).unwrap(); + let byte = text.lines().nth(line).unwrap().find(needle).unwrap(); + let col = text.lines().nth(line).unwrap()[..byte].encode_utf16().count() as u32 + offset; + syntax.java_configuration_site(text, Pos::new(line as u32, col), &|| false) + }; + // @Value arguments (literal, qualified annotation, constant key). + assert!(at("${app.port", 2)); + assert!(at("Keys.NAME", 6)); + // Lookup arguments, including a constant and the default value. + assert!(at("\"app.host\"", 3)); + assert!(at("\"h\"", 1)); + assert!(at("Keys.USER", 1)); + assert!(at("\"k\"", 1)); + // Ordinary Java: names, other annotations, other calls, the lookup's own name and receiver. + assert!(!at("@Value(\"$", 2)); + assert!(!at("int port", 4)); + assert!(!at("@Values", 9)); + assert!(!at("@Override", 2)); + assert!(!at("getProperty(\"app", 3)); + assert!(!at("env.getProperty(\"app", 1)); + assert!(!at("app.port\")", 1)); + assert!(!at("valueOf", 2)); + assert!(!at(".trim", 2)); + // Out of range and non-Java input. + assert!(!syntax.java_configuration_site(text, Pos::new(99, 0), &|| false)); + assert!(!syntax.java_configuration_site("", Pos::new(0, 0), &|| false)); + } #[test] fn yaml_structures_and_utf16() { let text = "app:\n 'time-out': 1\n hosts:\n - url: a\n - {url: b}\n map: {east: {size: 2}}\n---\n\"app.name\": ok\n'😀': {port: 3}\n"; diff --git a/crates/ide-language/src/syntax/lines.rs b/crates/ide-language/src/syntax/lines.rs index 5b8de2062..6fd0b1906 100644 --- a/crates/ide-language/src/syntax/lines.rs +++ b/crates/ide-language/src/syntax/lines.rs @@ -86,6 +86,16 @@ pub(crate) fn utf16_len(s: &str) -> u32 { if s.is_ascii() { s.len() as u32 } else { s.chars().map(|c| c.len_utf16() as u32).sum() } } +/// Byte offset of `pos` (line, UTF-16 column) in `text`; None past the last line. +pub(crate) fn byte_at(text: &str, pos: Pos) -> Option { + let mut start = 0; + for _ in 0..pos.line { + start += text[start..].find('\n')? + 1; + } + let line_end = text[start..].find('\n').map_or(text.len(), |i| start + i); + Some(start + utf16_to_byte(&text[start..line_end], pos.col as usize)) +} + pub(crate) fn utf16_to_byte(s: &str, offset: usize) -> usize { let mut units = 0; for (i, c) in s.char_indices() { diff --git a/crates/ide-language/src/syntax/web.rs b/crates/ide-language/src/syntax/web.rs index 73c9896d4..88a287570 100644 --- a/crates/ide-language/src/syntax/web.rs +++ b/crates/ide-language/src/syntax/web.rs @@ -6,7 +6,7 @@ //! `