mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 18:38:33 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fd25ab9b | ||
|
|
1f0b2a43e7 |
No files matched your search
@@ -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<ConfigurationKey> {
|
||||
let mut out = Vec::new();
|
||||
@@ -188,6 +222,50 @@ fn unescape_property(raw: &str) -> Option<String> {
|
||||
#[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";
|
||||
|
||||
@@ -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<usize> {
|
||||
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() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! `<style>` elements of HTML documents and feed the symbol index (see
|
||||
//! [`super::Syntax::tags`]), so a lookup is an exact index query.
|
||||
|
||||
use super::lines::{LineIndex, utf16_to_byte};
|
||||
use super::lines::{LineIndex, byte_at};
|
||||
use super::{Grammar, parse};
|
||||
use crate::SymbolTag;
|
||||
use ide_core::{Pos, Range, SymbolKind};
|
||||
@@ -130,15 +130,6 @@ pub(crate) fn definitions(g: &Grammar, css: Option<&Grammar>, tree: &Tree, text:
|
||||
out
|
||||
}
|
||||
|
||||
fn byte_at(text: &str, pos: Pos) -> Option<usize> {
|
||||
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))
|
||||
}
|
||||
|
||||
/// The whitespace-separated token of `value` (starting at byte `base`) around byte `at`.
|
||||
fn token_at(value: &str, base: usize, at: usize) -> Option<(usize, &str)> {
|
||||
let rel = at.checked_sub(base)?;
|
||||
|
||||
@@ -117,13 +117,25 @@ impl LspManager {
|
||||
return Ok(None);
|
||||
}
|
||||
// Definition in ordinary Java code remains JDT's definition. Reverse
|
||||
// configuration navigation is offered on string expressions; field/config
|
||||
// accessor usages are merged into the ordinary Find Usages response.
|
||||
// configuration navigation is offered only inside `@Value` / property
|
||||
// lookup arguments: the command builds a project-wide index, and every
|
||||
// Ctrl+click and Ctrl+hover probe would otherwise wait on it before JDT
|
||||
// answers. Field/config accessor usages are merged into Find Usages.
|
||||
if !config && !usages {
|
||||
let text = snap.text.as_str();
|
||||
if !text.contains("Value") && !text.contains("getProperty") && !text.contains("getRequiredProperty") {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = text.to_owned();
|
||||
let cancelled = cancel.clone();
|
||||
let site = tokio::task::spawn_blocking(move || {
|
||||
syntax().java_configuration_site(&text, pos, &|| cancelled.is_cancelled())
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !site {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let key = if config {
|
||||
let text = snap.text.as_str().to_owned();
|
||||
|
||||
@@ -3035,7 +3035,40 @@ async fn configuration_extension_failure_preserves_native_java_navigation() {
|
||||
"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 6}
|
||||
}}),
|
||||
);
|
||||
let result = h.mgr.goto(doc, Pos::new(0, 2), GotoKind::Definition).await.unwrap();
|
||||
let result = h.mgr.goto(doc, Pos::new(0, 12), GotoKind::Definition).await.unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(s.requested("workspace/executeCommand").len(), 1);
|
||||
assert_eq!(s.requested("textDocument/definition").len(), 1);
|
||||
}
|
||||
|
||||
/// The configuration command indexes the whole project, so Go to Definition (and
|
||||
/// every Ctrl+hover probe) consults it only inside a key argument; ordinary Java
|
||||
/// symbols in a Spring file go straight to JDT.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn java_definition_consults_configuration_only_inside_key_arguments() {
|
||||
let mut caps = incremental_caps();
|
||||
caps["executeCommandProvider"] = json!({"commands": ["openidea.java.configuration"]});
|
||||
let h = harness_for("java", "java", &[("jdtls", caps)], &[]);
|
||||
let s = h.launcher.state("jdtls");
|
||||
let text = "class A {\n @Value(\"${app.port}\") int port;\n String a() { return env.getProperty(Keys.HOST) + x.getValue(); }\n}\n";
|
||||
let (doc, path) = h.open("A.java", text);
|
||||
h.wait_status("jdtls", ServerStatus::Ready).await;
|
||||
s.respond("workspace/executeCommand", json!({"locations": [], "keys": [], "roots": []}));
|
||||
s.respond(
|
||||
"textDocument/definition",
|
||||
json!({"uri": uri_of(&path), "range": {
|
||||
"start": {"line": 0, "character": 6}, "end": {"line": 0, "character": 7}
|
||||
}}),
|
||||
);
|
||||
// Field type, field name, method name, the lookup's receiver and name, another call.
|
||||
for pos in [Pos::new(1, 24), Pos::new(1, 28), Pos::new(2, 9), Pos::new(2, 22), Pos::new(2, 28), Pos::new(2, 53)] {
|
||||
assert_eq!(h.mgr.goto(doc, pos, GotoKind::Definition).await.unwrap().len(), 1, "{pos:?}");
|
||||
}
|
||||
assert!(s.requested("workspace/executeCommand").is_empty());
|
||||
assert_eq!(s.requested("textDocument/definition").len(), 6);
|
||||
// Inside a placeholder and a constant lookup key: configuration first, JDT as fallback.
|
||||
for pos in [Pos::new(1, 13), Pos::new(2, 41)] {
|
||||
h.mgr.goto(doc, pos, GotoKind::Definition).await.unwrap();
|
||||
}
|
||||
assert_eq!(s.requested("workspace/executeCommand").len(), 2);
|
||||
}
|
||||
@@ -1173,7 +1173,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.2.0.jar");
|
||||
let extension = data_dir.join("java-usages-1.2.1.jar");
|
||||
let bytes = include_bytes!("../../../plugins/java-usages/openidea-java-usages.jar");
|
||||
if std::fs::read(&extension).is_ok_and(|old| old == bytes) {
|
||||
clean_usages_bundles(data_dir, &extension);
|
||||
|
||||
@@ -53,14 +53,17 @@ files for disk discovery. It does not guess a global association from matching
|
||||
text in unrelated projects.
|
||||
|
||||
Indexing is lazy. The first explicit navigation/search builds a compact snapshot;
|
||||
ordinary typing, code-lens probes and bulk scoped reference analysis do not start indexing. At most eight project snapshots are
|
||||
ordinary typing, code-lens probes, bulk scoped reference analysis, and Java Go to Definition / Ctrl+hover outside a `@Value` or property-lookup argument (checked with the bundled tree-sitter grammar) do not start indexing. At most eight project snapshots are
|
||||
retained, with a shared 100,000-association retention cap. Records contain property names, element handles and source locations;
|
||||
ASTs and bindings are discarded after indexing. A lexical prefilter avoids AST
|
||||
construction for ordinary Java units, but every accepted association is resolved
|
||||
semantically. Warm requests reuse the snapshot. Java model/reconcile changes
|
||||
invalidate it conservatively, including dependent-type changes; open working-copy
|
||||
content fingerprints also prevent stale answers before reconciliation. This is
|
||||
**snapshot invalidation**, not a fully incremental Java semantic index.
|
||||
semantically. Warm requests reuse the snapshot. Java model changes (sources on
|
||||
disk, classpath, archives) invalidate it conservatively, including dependent-type
|
||||
changes; a fingerprint of working copies with unsaved changes covers edits before
|
||||
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.
|
||||
|
||||
Metadata/JAR timestamps are checked before reuse. The client caches parsed disk
|
||||
configuration files by modification time and size (at most 100,000 cached keys), overlays open buffers, and
|
||||
|
||||
@@ -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.2.0
|
||||
Bundle-Version: 1.2.1
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
|
||||
org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime,
|
||||
|
||||
@@ -57,7 +57,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.2.0.jar`, and added alongside existing initialization bundles.
|
||||
`java-usages-1.2.1.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.
|
||||
|
||||
@@ -70,6 +70,9 @@ and compare SHA-256. The prior 1.1.1 output was
|
||||
`1e569939cba22c7db6e2c9b53de2a4c10ccaabe762a51427119a04af3d974ae4`.
|
||||
Version 1.2.0 adds the configuration command; its reproducible SHA-256 is
|
||||
`9140f1d2ecf27eadfe66b40a17daa18a08a65927ebef79456ba55d486ce5eeba`.
|
||||
Version 1.2.1 keeps the configuration snapshot across editor open/close, saves of
|
||||
unrelated content and non-Java resource changes; its reproducible SHA-256 is
|
||||
`cbae3e3a0bcf2818b4cc9d98998bd9b81f92e1ae7803dc757134485f9776a4e1`.
|
||||
|
||||
```sh
|
||||
export PATH=/path/to/jdk25/bin:$PATH
|
||||
|
||||
Binary file not shown.
@@ -27,9 +27,23 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
private static final String VALUE = "org.springframework.beans.factory.annotation.Value";
|
||||
private static final String RESOLVER = "org.springframework.core.env.PropertyResolver";
|
||||
private static final int MAX_UNITS = 20_000, MAX_ENTRIES = 100_000, MAX_BYTES = 2 * 1024 * 1024;
|
||||
private static final AtomicLong REVISION = new AtomicLong();
|
||||
private static final AtomicLong REVISION = new AtomicLong(), GENERATION = new AtomicLong();
|
||||
private static final Map<String, Index> CACHE = new LinkedHashMap<>();
|
||||
static { JavaCore.addElementChangedListener(event -> REVISION.incrementAndGet(), ElementChangedEvent.POST_CHANGE | ElementChangedEvent.POST_RECONCILE); }
|
||||
/** Unsaved edits are covered by the working-copy digest, so reconciles never
|
||||
* invalidate. Opening/closing a working copy (F_PRIMARY_WORKING_COPY) and
|
||||
* non-Java resources, reported as F_CONTENT on a project, root or package
|
||||
* (configuration files are parsed by the client; metadata files carry their
|
||||
* 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); }
|
||||
static boolean affects(IJavaElementDelta delta) {
|
||||
int flags = delta.getFlags() & ~IGNORED_FLAGS;
|
||||
if (delta.getElement().getElementType() < IJavaElement.COMPILATION_UNIT) flags &= ~IJavaElementDelta.F_CONTENT;
|
||||
if (delta.getKind() != IJavaElementDelta.CHANGED || flags != 0) return true;
|
||||
for (IJavaElementDelta child : delta.getAffectedChildren()) if (affects(child)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private record Entry(String name, String handle, Location location, boolean relaxed) {}
|
||||
private record FileStamp(java.nio.file.attribute.FileTime modified, long size) {}
|
||||
@@ -38,7 +52,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
catch (java.io.IOException e) { return new FileStamp(null, -1); }
|
||||
}
|
||||
private record Index(long revision, String workingCopies, List<Entry> entries, List<String> roots,
|
||||
Map<Path, FileStamp> files, boolean incomplete, int parsed) {}
|
||||
Map<Path, FileStamp> files, boolean incomplete, int parsed, long generation) {}
|
||||
|
||||
@Override
|
||||
public Object executeCommand(String command, List<Object> args, IProgressMonitor monitor) throws Exception {
|
||||
@@ -108,7 +122,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
}
|
||||
return Map.of("locations", locations, "keys", keys, "roots", index.roots,
|
||||
"incomplete", index.incomplete || locations.size() >= 2000,
|
||||
"parsedUnits", index.parsed, "revision", index.revision);
|
||||
"parsedUnits", index.parsed, "revision", index.revision, "generation", index.generation);
|
||||
}
|
||||
|
||||
private static boolean sameUri(String a, String b) {
|
||||
@@ -156,9 +170,12 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
for (ICompilationUnit copy : JavaCore.getWorkingCopies(null)) copies.put(copy.getPrimary().getHandleIdentifier(), copy);
|
||||
for (ICompilationUnit copy : JavaCore.getWorkingCopies(JavaLanguageServerPlugin.getInstance().getWorkingCopyOwner()))
|
||||
copies.put(copy.getPrimary().getHandleIdentifier(), copy);
|
||||
// Only unsaved contents differ from what the Java model reports for disk,
|
||||
// so merely opening or closing an editor keeps the snapshot.
|
||||
var digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||
for (var entry : copies.entrySet()) {
|
||||
check(monitor);
|
||||
if (!entry.getValue().hasUnsavedChanges()) continue;
|
||||
digest.update(entry.getKey().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
String source = entry.getValue().getSource();
|
||||
if (source != null) digest.update(source.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
@@ -209,7 +226,7 @@ public final class ConfigurationCommand implements IDelegateCommandHandler {
|
||||
// Return project locations as well; the client excludes nested independent modules.
|
||||
roots.add(project.getProject().getLocationURI().toString());
|
||||
builder.metadataDirectory(project.getProject().getLocation().toFile().toPath().resolve("src/main/resources"), project);
|
||||
Index result = new Index(revision, workingCopies, List.copyOf(builder.entries), List.copyOf(roots), Map.copyOf(builder.files), builder.incomplete, builder.parsed);
|
||||
Index result = new Index(revision, workingCopies, List.copyOf(builder.entries), List.copyOf(roots), Map.copyOf(builder.files), builder.incomplete, builder.parsed, GENERATION.incrementAndGet());
|
||||
CACHE.remove(id);
|
||||
while (!CACHE.isEmpty() && (CACHE.size() >= 8 ||
|
||||
CACHE.values().stream().mapToInt(value -> value.entries.size()).sum() + result.entries.size() > MAX_ENTRIES))
|
||||
|
||||
@@ -200,6 +200,33 @@ try:
|
||||
warm = []
|
||||
for _ in range(20):
|
||||
start = time.perf_counter(); query('app.time-out'); warm.append(time.perf_counter() - start)
|
||||
# The snapshot stays warm across editor activity that cannot change it:
|
||||
# opening/closing another file, a full build, and an edit once it is saved.
|
||||
generation = query('app.time-out')['generation']
|
||||
other = source_dir / 'demo/NotSpring.java'
|
||||
other_text = other.read_text()
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': other.as_uri(), 'languageId': 'java', 'version': 1, 'text': other_text}}})
|
||||
assert query('app.time-out')['generation'] == generation, 'opening an editor rebuilt the index'
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': other.as_uri()}}})
|
||||
request('java/buildWorkspace', True)
|
||||
assert query('app.time-out')['generation'] == generation, 'closing an editor or building rebuilt the index'
|
||||
saved_text = other_text + '// saved\n'
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': other.as_uri(), 'languageId': 'java', 'version': 1, 'text': other_text}}})
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didChange', 'params': {'textDocument': {'uri': other.as_uri(), 'version': 2}, 'contentChanges': [{'text': saved_text}]}})
|
||||
other.write_text(saved_text)
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didSave', 'params': {'textDocument': {'uri': other.as_uri()}}})
|
||||
after_save = query('app.time-out')['generation']
|
||||
assert query('app.time-out')['generation'] == after_save, 'a saved editor keeps rebuilding the index'
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': other.as_uri()}}})
|
||||
after_close = query('app.time-out')['generation']
|
||||
assert query('app.time-out')['generation'] == after_close
|
||||
# A source added on disk still invalidates it.
|
||||
added = write('demo/Added.java', 'package demo; @org.springframework.boot.context.properties.ConfigurationProperties("added") class Added { int size; }')
|
||||
send({'jsonrpc': '2.0', 'method': 'workspace/didChangeWatchedFiles', 'params': {'changes': [{'uri': added.as_uri(), 'type': 1}]}})
|
||||
deadline = time.monotonic() + 30
|
||||
while not query('added.size')['locations']:
|
||||
assert time.monotonic() < deadline, 'a new source on disk did not reach the index'
|
||||
time.sleep(0.2)
|
||||
# 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}]}})
|
||||
@@ -211,7 +238,7 @@ try:
|
||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didClose', 'params': {'textDocument': {'uri': file.as_uri()}}})
|
||||
restored = query('app.nested.url')
|
||||
assert restored['locations'], restored
|
||||
evidence = {'stress': os.environ.get('OPENIDEA_CONFIGURATION_STRESS') == '1', 'editRebuildMs': edit_ms, 'cases': results, 'warmMedianMs': sorted(warm)[len(warm)//2] * 1000, 'warmMaxMs': max(warm) * 1000}
|
||||
evidence = {'stress': os.environ.get('OPENIDEA_CONFIGURATION_STRESS') == '1', 'editRebuildMs': edit_ms, 'cases': results, 'warmMedianMs': sorted(warm)[len(warm)//2] * 1000, 'warmMaxMs': max(warm) * 1000, 'generations': [generation, after_save, after_close]}
|
||||
(work / 'evidence.json').write_text(json.dumps(evidence, indent=2))
|
||||
print(json.dumps(evidence, indent=2))
|
||||
finally:
|
||||
|
||||
Reference in new issue
Block a user