Compare commits

...
Author SHA1 Message Date
l0ng-ai b550be9685 feat(plugins): open plugin API 1.2
API 1.1.0 shipped with 0.1.37 and its WIT is frozen in the released
snapshot, so additions now go into 1.2.0. The package version in wit/
and the SDK's copy, ENGINE_VERSION and both API_VERSION constants move to
1.2; the official plugins and test fixtures are rebuilt against it.

No interface, type or function changes: 1.0 and 1.1 components keep
linking. The release that ships 1.2 copies wit/ over
sdk/rust/openidea-plugin/tests/api/released/.
2026-11-06 09:29:03 +08:00
l0ng-ai a6474539b8 fix(remote): build the Docker run tests' helpers only where they run (#1645)
The container command tests are Unix-only, so on Windows their helpers and the PathBuf import were unused and clippy -D warnings failed the Rust (windows-latest) job.
2026-11-06 08:35:22 +08:00
l0ng-ai 401b4a9441 fix(java): list dependencies' implementations after the project's in Go to Implementation (#1644)
Go to Implementation on a type or method declared in a dependency listed only the project's implementations once the project had one. openidea.java.implementations (java-usages 1.5.0) now answers {locations, complete}, complete when the type is declared in the sources, and also searches the Kotlin bridge's classes. When not complete, the chooser opens with the project's implementations (searching…) through the new lsp_goto_implementation channel and fills in the dependencies' when the standard request ends; closing it cancels the search.
2026-11-06 08:16:13 +08:00
68 changed files with 487 additions and 104 deletions

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ use std::path::{Component, Path, PathBuf};
pub const MANIFEST_FILE: &str = "plugin.toml";
/// The plugin API (engine) version this host implements.
pub const ENGINE_VERSION: &str = "1.1.0";
pub const ENGINE_VERSION: &str = "1.2.0";
/// The 0.1 API, still accepted for contribution-only packages and 0.1 components.
pub const LEGACY_API: &str = "0.1";
/// Marketplace categories, in the order they are shown.
+26
View File
@@ -257,6 +257,28 @@ pub async fn lsp_implementations(
result
}
/// Go to Implementation of the symbol at `pos`. Implementations found before a slower
/// search for the rest (Java: the project's, before the dependencies') arrive on
/// `on_partial`; the result has them all. Cancel with `cancel_request(request)`.
pub async fn lsp_goto_implementation(
host: &Host,
window: &str,
request: RequestId,
doc: DocId,
pos: Pos,
on_partial: super::Channel<Vec<Location>>,
) -> IdeResult<Vec<Location>> {
let lsp = host.project(window)?.lsp.clone();
let cancel = host.begin_request(request);
let result = lsp
.goto_implementation(doc, pos, cancel, &|locs| {
let _ = on_partial.send(locs);
})
.await;
host.end_request(request);
result
}
/// Contents of a read-only language-server document, including JDT library
/// classes and disk class files. Cached per project.
pub async fn lsp_virtual_document(host: &Host, window: &str, uri: String) -> IdeResult<VirtualDocument> {
@@ -746,6 +768,10 @@ pub(crate) fn register(d: &mut Dispatcher) {
d.add("lsp_implementations", |c| async move {
lsp_implementations(&c.host, &c.window, c.arg("request")?, c.arg("doc")?, c.arg("positions")?).await
});
d.add("lsp_goto_implementation", |c| async move {
let on_partial = c.sender("onPartial")?;
lsp_goto_implementation(&c.host, &c.window, c.arg("request")?, c.arg("doc")?, c.arg("pos")?, on_partial).await
});
d.add(
"lsp_resolve_class_file",
|c| async move { lsp_resolve_class_file(&c.host, &c.window, c.arg("path")?).await },
+71 -15
View File
@@ -317,6 +317,25 @@ async fn library_sources(snap: &DocSnap, pos: Pos, locs: Vec<Location>) -> Vec<L
const JAVA_IMPLEMENTATIONS: &str = "openidea.java.implementations";
/// The answer of [`JAVA_IMPLEMENTATIONS`]: `complete` when the type is declared in the
/// sources, so no dependency can implement it.
#[derive(serde::Deserialize)]
struct SourceImplementations {
locations: Vec<lsp::Location>,
complete: bool,
}
/// Receives the implementations found so far, before a slower search for the rest.
type Partial<'a> = Option<&'a (dyn Fn(Vec<Location>) + Send + Sync)>;
/// `first`, then those of `more` not in it (same file and start).
fn merge_locations(first: Vec<Location>, more: Vec<Location>) -> Vec<Location> {
let mut seen: HashSet<(PathBuf, Pos)> = first.iter().map(|l| (l.path.to_path_buf(), l.range.start)).collect();
let mut out = first;
out.extend(more.into_iter().filter(|l| seen.insert((l.path.to_path_buf(), l.range.start))));
out
}
fn goto_method(kind: GotoKind) -> (&'static str, &'static str, &'static str) {
match kind {
GotoKind::Definition => ("textDocument/definition", "definitionProvider", "go to definition"),
@@ -1231,7 +1250,22 @@ impl LspManager {
}
let (method, cap, feature) = goto_method(kind);
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
self.goto_in(&snap, &targets, method, pos, None).await
self.goto_in(&snap, &targets, method, pos, None, None).await
}
/// Go to Implementation, with the implementations found first passed to `partial` while
/// the search goes on: jdtls answers from the project's sources quickly, then searches every
/// dependency jar for a type declared in one. Returns them all, the first ones first.
pub async fn goto_implementation(
&self,
doc: DocId,
pos: Pos,
cancel: CancellationToken,
partial: &(dyn Fn(Vec<Location>) + Send + Sync),
) -> IdeResult<Vec<Location>> {
let (method, cap, feature) = goto_method(GotoKind::Implementation);
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
self.goto_in(&snap, &targets, method, pos, Some(&cancel), Some(partial)).await
}
/// Implementations of the symbols at each of `positions` (gutter line
@@ -1248,7 +1282,7 @@ impl LspManager {
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
let requests = positions.iter().copied().map(|pos| {
let (snap, targets, cancel) = (&snap, &targets, &cancel);
async move { self.goto_in(snap, targets, method, pos, Some(cancel)).await }
async move { self.goto_in(snap, targets, method, pos, Some(cancel), None).await }
});
let results: Vec<IdeResult<Vec<Location>>> =
futures::stream::iter(requests).buffered(IMPLEMENTATION_CONCURRENCY).collect().await;
@@ -1261,23 +1295,23 @@ impl LspManager {
Ok(results.into_iter().map(Result::unwrap_or_default).collect())
}
/// jdtls: the implementations in the project's sources (`openidea.java.implementations`).
/// Its standard request builds the type hierarchy over every dependency jar too, which takes
/// long in a project with many dependencies; empty (or no extension) falls back to it.
/// jdtls: the implementations in the project's sources (`openidea.java.implementations`),
/// and whether they are all there are. Its standard request builds the type hierarchy over
/// every dependency jar too, which takes long in a project with many dependencies.
async fn java_source_implementations(
&self,
snap: &DocSnap,
t: &Target,
pos: Pos,
cancel: Option<&CancellationToken>,
) -> Vec<Location> {
) -> (Vec<Location>, bool) {
if t.server.name != "jdtls" || !t.caps().commands().iter().any(|c| c == JAVA_IMPLEMENTATIONS) {
return Vec::new();
return (Vec::new(), false);
}
let params = snap.pos_params(t, pos);
let arguments =
json!([params["textDocument"]["uri"], params["position"]["line"], params["position"]["character"]]);
let result: IdeResult<Vec<lsp::Location>> = self
let result: IdeResult<SourceImplementations> = self
.call(
t,
"workspace/executeCommand",
@@ -1287,10 +1321,16 @@ impl LspManager {
)
.await;
match result {
Ok(locs) => convert::goto_response(&self.conv(t.enc()), lsp::GotoDefinitionResponse::Array(locs)),
Ok(found) => {
let locs =
convert::goto_response(&self.conv(t.enc()), lsp::GotoDefinitionResponse::Array(found.locations));
let locs = library_sources(snap, pos, locs).await;
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
(locs, found.complete)
}
Err(e) => {
tracing::debug!("{JAVA_IMPLEMENTATIONS}: {}", e.message);
Vec::new()
(Vec::new(), false)
}
}
}
@@ -1302,16 +1342,23 @@ impl LspManager {
method: &str,
pos: Pos,
cancel: Option<&CancellationToken>,
partial: Partial<'_>,
) -> IdeResult<Vec<Location>> {
let mut out = Outcome::new();
for t in targets.iter().filter(|t| snap.asks(t, pos)) {
// Found in the sources before the standard request searches the dependencies too.
let mut first = Vec::new();
if method == "textDocument/implementation" {
let locs = self.java_source_implementations(snap, t, pos, cancel).await;
if !locs.is_empty() {
let locs = library_sources(snap, pos, locs).await;
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
let (locs, complete) = self.java_source_implementations(snap, t, pos, cancel).await;
if complete && !locs.is_empty() {
return Ok(locs);
}
if let Some(partial) = partial
&& !locs.is_empty()
{
partial(locs.clone());
first = locs;
}
}
let r = loading::while_loading(&t.server, cancel, loading::LOADING_WAIT, goto_is_empty, || {
self.call::<_, Option<lsp::GotoDefinitionResponse>>(
@@ -1328,12 +1375,19 @@ impl LspManager {
let locs = convert::goto_response(&self.conv(t.enc()), r);
let locs = library_sources(snap, pos, locs).await;
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
let locs = merge_locations(first, locs);
if !locs.is_empty() {
return Ok(locs);
}
out.value = Some(Vec::new());
}
Ok(None) if !first.is_empty() => return Ok(first),
Ok(None) => out.value = Some(Vec::new()),
// Those shown stay valid when the rest can't be found.
Err(e) if !first.is_empty() && e.kind != ErrorKind::Cancelled => {
tracing::debug!("go to implementation in the dependencies: {}", e.message);
return Ok(first);
}
Err(e) => out.fail(e),
}
}
@@ -1517,7 +1571,9 @@ impl LspManager {
return Vec::new();
}
let defining = self.ready_targets(&snap.servers, |c| c.has("definitionProvider")).await;
self.goto_in(&snap, &defining, "textDocument/definition", pos, Some(&cancel)).await.unwrap_or_default()
self.goto_in(&snap, &defining, "textDocument/definition", pos, Some(&cancel), None)
.await
.unwrap_or_default()
};
let (results, declarations) = futures::join!(join_all(requests), declarations);
let mut out = Outcome::new();
+42 -8
View File
@@ -1816,23 +1816,57 @@ async fn java_implementations_come_from_sources_first() {
h.wait_status("jdtls", ServerStatus::Ready).await;
let s = h.launcher.state("jdtls");
let uri = uri_of(&path);
let at = move |line: u32| json!([{ "uri": uri, "range": {"start":{"line":line,"character":6},"end":{"line":line,"character":7}} }]);
let at = move |lines: &[u32]| {
lines
.iter()
.map(|&l| json!({ "uri": uri, "range": {"start":{"line":l,"character":6},"end":{"line":l,"character":7}} }))
.collect::<Value>()
};
*s.responder.lock() = Some(Arc::new(move |_, method, params| match method {
// The sources have an implementation of line 0 only.
"workspace/executeCommand" => Some(if params["arguments"][1] == 0 { at(1) } else { json!([]) }),
"textDocument/implementation" => Some(at(3)),
// Line 0 is declared in the sources; line 2 in a dependency, which implements it 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 }),
_ => json!({ "locations": [], "complete": true }),
}),
"textDocument/implementation" => Some(at(&[3, 1])),
_ => None,
}));
let lines = |locs: &[ide_core::Location]| locs.iter().map(|l| l.range.start.line).collect::<Vec<_>>();
let found = h.mgr.goto(doc, Pos::new(0, 10), GotoKind::Implementation).await.unwrap();
assert_eq!(found.iter().map(|l| l.range.start.line).collect::<Vec<_>>(), vec![1]);
assert_eq!(lines(&found), vec![1]);
let markers = h.mgr.implementations(doc, &[Pos::new(0, 10)], Default::default()).await.unwrap();
assert_eq!(lines(&markers[0]), vec![1]);
assert!(s.requested("textDocument/implementation").is_empty(), "no search through the dependencies");
let asked = s.requested("workspace/executeCommand");
assert_eq!(asked[0]["command"], "openidea.java.implementations");
assert_eq!((asked[0]["arguments"][1].clone(), asked[0]["arguments"][2].clone()), (json!(0), json!(10)));
// None in the sources: the standard request, with the dependencies.
let found = h.mgr.goto(doc, Pos::new(2, 6), GotoKind::Implementation).await.unwrap();
assert_eq!(found.iter().map(|l| l.range.start.line).collect::<Vec<_>>(), vec![3]);
// A dependency's type: those in the sources first, then the dependencies' too.
let partial = Mutex::new(Vec::new());
let found = h
.mgr
.goto_implementation(doc, Pos::new(2, 6), Default::default(), &|locs| partial.lock().push(lines(&locs)))
.await
.unwrap();
assert_eq!(*partial.lock(), vec![vec![1]]);
assert_eq!(lines(&found), vec![1, 3]);
assert_eq!(s.requested("textDocument/implementation").len(), 1);
// Without a receiver for the first ones (gutter markers), just all of them.
let found = h.mgr.goto(doc, Pos::new(2, 6), GotoKind::Implementation).await.unwrap();
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.
let partial = Mutex::new(Vec::new());
let found = h
.mgr
.goto_implementation(doc, Pos::new(3, 6), Default::default(), &|locs| partial.lock().push(lines(&locs)))
.await
.unwrap();
assert!(partial.lock().is_empty());
assert_eq!(lines(&found), vec![3, 1]);
assert_eq!(s.requested("textDocument/implementation").len(), 3);
}
#[tokio::test(flavor = "multi_thread")]
+1 -1
View File
@@ -33,7 +33,7 @@ use views_bindings::exports::openidea::plugin::view_provider as wit_view;
use views_bindings::openidea::plugin::view_types as wit_vt;
/// The API version of the world above, `major.minor` of [`host_api`].
pub const API_VERSION: &str = "1.1";
pub const API_VERSION: &str = "1.2";
const EPOCH_TICK: Duration = Duration::from_millis(10);
pub(crate) const MAX_TEXT: usize = 1 << 20;
pub(crate) const MAX_TITLE: usize = 256;
Binary file not shown.
@@ -1 +1 @@
455d848dd27f242bb3e9bf420558cbc8c200853f51ada3d481635ac9771803f3
60d67ab91d3c42127500481510c43d829e07babcab872f94a6189d27c901234a
Binary file not shown.
@@ -1 +1 @@
f7e1ae06a90460ed1d2319bfc51f848654df145fa451f3d959b32776b2b25ce3
6e9aab775c231987a73d2e424f8cd389e429f525327fac4f5aba550ebc53afe6
Binary file not shown.
@@ -1 +1 @@
12973c7ca7330fc50eb16718fd769d412401b5a92bd014f4804659e4a272122f
ab066ebab16ba774c2f649a72469bcd25aaa1ffaee77f70ea67395ea26ddee02
+2 -2
View File
@@ -5,8 +5,8 @@ wit_bindgen::generate!({
inline: "
package fixture:misbehave;
world misbehave {
include openidea:plugin/plugin-syntax@1.1.0;
export openidea:plugin/providers@1.1.0;
include openidea:plugin/plugin-syntax@1.2.0;
export openidea:plugin/providers@1.2.0;
}
",
path: "../../../../../wit",
+3
View File
@@ -193,8 +193,10 @@ pub fn compose_container_root(config_json: &str, service: &str, root: &Path) ->
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
fn config(command: &str, args: &[&str]) -> RunConfig {
RunConfig {
id: "app".into(),
@@ -212,6 +214,7 @@ mod tests {
}
}
#[cfg(unix)]
fn run<'a>(
target: &'a RunTarget,
endpoint: &'a BTreeMap<String, String>,
+1 -1
View File
@@ -1169,7 +1169,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.4.0.jar");
let extension = data_dir.join("java-usages-1.5.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);
+1 -1
View File
@@ -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.4.0
Bundle-Version: 1.5.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,
+7 -1
View File
@@ -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.4.0.jar`, and added alongside existing initialization bundles.
`java-usages-1.5.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.
@@ -78,6 +78,11 @@ Version 1.3.0 adds the class-hierarchy refactoring commands; its reproducible SH
Version 1.4.0 adds `openidea.java.implementations` (Go to Implementation in the workspace's
source folders, before the standard request that also searches every dependency jar); its
reproducible SHA-256 is `d7601493eda846811b4bb8bfe7f3d56e3234a45e663648210db3937cebc770a5`.
Version 1.5.0 answers `{locations, complete}`: `complete` when the type is declared in the sources
(no dependency implements it), otherwise the app also runs the standard request and adds the
dependencies' implementations after the first ones. The search also covers the project's Kotlin
classes attached by the Kotlin bridge; its reproducible SHA-256 is
`7588748c2797f9da2beb209714a75c29b4e31b155f095329bfb7e399d807c20a`.
```sh
export PATH=/path/to/jdk25/bin:$PATH
@@ -107,6 +112,7 @@ python3 java/java-usages/tests/verify.py /path/to/jdtls /path/to/lombok.jar
python3 java/java-usages/tests/read-write.py /path/to/jdtls /path/to/lombok.jar > /tmp/read-write.json
OPENIDEA_USAGES_INDEX_STRESS=1 python3 java/java-usages/tests/read-write.py /path/to/jdtls /path/to/lombok.jar > /tmp/read-write-indexed.json
python3 java/java-usages/tests/hierarchy.py /path/to/jdtls
python3 java/java-usages/tests/implementations.py /path/to/jdtls
```
The harnesses select the JDT LS configuration for the current platform. The first
Binary file not shown.
@@ -2,6 +2,7 @@ package ai.openidea.implementations;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
@@ -13,39 +14,52 @@ import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.lsp4j.Location;
/**
* Implementations of the type or method at a position, looked up in the workspace's source
* folders only. The standard request builds the type hierarchy over every dependency jar too,
* which takes long in a project with many dependencies; the client falls back to it when the
* sources have none.
* Implementations of the type or method at a position, looked up in the workspace's own code
* only (source folders and bridged Kotlin classes): {@code {locations, complete}}. The standard
* request builds the type hierarchy over every dependency jar too, which takes long in a project
* with many dependencies. {@code complete} when the type is declared in the sources, which no
* dependency extends; otherwise the client also asks the standard request for the dependencies'
* implementations.
*/
public final class ImplementationsCommand implements IDelegateCommandHandler {
/** Classpath attribute of the Kotlin bridge's library entries (`java/kotlin-bridge`). */
private static final String KOTLIN_OUTPUT = "ai.openidea.kotlin";
@Override
public Object executeCommand(String command, List<Object> arguments, IProgressMonitor monitor) throws Exception {
List<Location> locations = new ArrayList<>();
boolean complete = find(arguments, locations, monitor);
return Map.of("locations", locations, "complete", complete);
}
/** Adds the implementations to {@code result}; whether they are all there are. */
private static boolean find(List<Object> arguments, List<Location> result, IProgressMonitor monitor) throws Exception {
String uri = (String) arguments.get(0);
int line = ((Number) arguments.get(1)).intValue();
int column = ((Number) arguments.get(2)).intValue();
List<Location> result = new ArrayList<>();
ITypeRoot root = JDTUtils.resolveTypeRoot(uri);
if (root == null) return result;
if (root == null) return false;
var preferences = JavaLanguageServerPlugin.getPreferencesManager();
IJavaElement element = JDTUtils.findElementAtSelection(root, line, column, preferences, monitor);
IMethod method = null;
IType type;
if (element instanceof IMethod m) {
int flags = m.getFlags();
if (m.isConstructor() || Flags.isStatic(flags) || Flags.isPrivate(flags) || Flags.isFinal(flags)) return result;
if (m.isConstructor() || Flags.isStatic(flags) || Flags.isPrivate(flags) || Flags.isFinal(flags)) return false;
method = m;
type = m.getDeclaringType();
} else if (element instanceof IType t) {
type = t;
} else {
return result;
return false;
}
if (type == null || type.getJavaProject() == null) return result;
if (type == null || type.getJavaProject() == null) return false;
IRegion sources = JavaCore.newRegion();
for (IJavaProject project : JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()).getJavaProjects()) {
for (IPackageFragmentRoot fragmentRoot : project.getPackageFragmentRoots()) {
if (fragmentRoot.getKind() == IPackageFragmentRoot.K_SOURCE) sources.add(fragmentRoot);
if (fragmentRoot.getKind() == IPackageFragmentRoot.K_SOURCE || isKotlinOutput(fragmentRoot)) {
sources.add(fragmentRoot);
}
}
}
ITypeHierarchy hierarchy = type.getJavaProject().newTypeHierarchy(type, sources, monitor);
@@ -62,6 +76,17 @@ public final class ImplementationsCommand implements IDelegateCommandHandler {
Location location = found == null ? null : JDTUtils.toLocation(found);
if (location != null) result.add(location);
}
return result;
return !type.isBinary();
}
/** The project's compiled Kotlin classes, attached as a library by the Kotlin bridge. */
private static boolean isKotlinOutput(IPackageFragmentRoot root) throws JavaModelException {
if (root.getKind() != IPackageFragmentRoot.K_BINARY) return false;
IClasspathEntry entry = root.getRawClasspathEntry();
if (entry == null) return false;
for (IClasspathAttribute attribute : entry.getExtraAttributes()) {
if (KOTLIN_OUTPUT.equals(attribute.getName()) && "true".equals(attribute.getValue())) return true;
}
return false;
}
}
+19 -13
View File
@@ -24,7 +24,8 @@ config_source = next(home / name / 'config.ini' for name in config_names if (hom
project = work / 'project'
(project / 'src/zoo').mkdir(parents=True)
(project / '.project').write_text('<projectDescription><name>zoo</name><buildSpec><buildCommand><name>org.eclipse.jdt.core.javabuilder</name></buildCommand></buildSpec><natures><nature>org.eclipse.jdt.core.javanature</nature></natures></projectDescription>')
(project / '.classpath').write_text('<classpath><classpathentry kind="src" path="src"/><classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/><classpathentry kind="output" path="bin"/></classpath>')
# `kotlin`: the project's compiled Kotlin classes, as the Kotlin bridge attaches them.
(project / '.classpath').write_text('<classpath><classpathentry kind="src" path="src"/><classpathentry kind="lib" path="kotlin"><attributes><attribute name="ai.openidea.kotlin" value="true"/></attributes></classpathentry><classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/><classpathentry kind="output" path="bin"/></classpath>')
sources = {
'Repo.java': '''package zoo;
@@ -88,6 +89,10 @@ public class Use {
for name, text in sources.items():
(project / 'src/zoo' / name).write_text(text)
uri = lambda name: (project / 'src/zoo' / name).as_uri()
# Stands in for a Kotlin class of the project implementing the Java interface.
(work / 'kotlin-src/zoo').mkdir(parents=True)
(work / 'kotlin-src/zoo/Longs.java').write_text('package zoo;\n\npublic class Longs implements Repo<Long> {\n public void save(Long item) {\n }\n}\n')
subprocess.run(['javac', '-implicit:none', '-sourcepath', str(project / 'src'), '-d', str(project / 'kotlin'), str(work / 'kotlin-src/zoo/Longs.java')], check=True)
log = (work / 'stderr.log').open('w')
proc = subprocess.Popen(['java', '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Declipse.product=org.eclipse.jdt.ls.core.product', '-Xmx1G', '--add-modules=ALL-SYSTEM', '--add-opens', 'java.base/java.util=ALL-UNNAMED', '--add-opens', 'java.base/java.lang=ALL-UNNAMED', '-jar', str(next((home / 'plugins').glob('org.eclipse.equinox.launcher_*.jar'))), '-configuration', str(config), '-data', str(work / 'metadata')], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log)
@@ -154,30 +159,31 @@ def implementations(file, needle):
line, col = position(file, needle)
found = command('openidea.java.implementations', uri(file), line, col)
names = []
for loc in found:
for loc in found['locations']:
name = pathlib.Path(urllib.parse.unquote(urllib.parse.urlparse(loc['uri']).path)).name
names.append(f"{name}:{loc['range']['start']['line']}")
return sorted(names)
return sorted(names), found['complete']
try:
init = request('initialize', {'processId': None, 'rootUri': project.as_uri(), 'workspaceFolders': [{'uri': project.as_uri(), 'name': 'zoo'}], 'capabilities': {}, 'initializationOptions': {'bundles': [str(bundle)], 'settings': {'java': {'import': {'gradle': {'enabled': False}, 'maven': {'enabled': False}}}}}})
init = request('initialize', {'processId': None, 'rootUri': project.as_uri(), 'workspaceFolders': [{'uri': project.as_uri(), 'name': 'zoo'}], 'capabilities': {}, 'initializationOptions': {'bundles': [str(bundle)], 'extendedClientCapabilities': {'classFileContentsSupport': True}, 'settings': {'java': {'import': {'gradle': {'enabled': False}, 'maven': {'enabled': False}}}}}})
assert 'openidea.java.implementations' in init['capabilities']['executeCommandProvider']['commands']
send({'jsonrpc': '2.0', 'method': 'initialized', 'params': {}})
for name, text in sources.items():
send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': uri(name), 'languageId': 'java', 'version': 1, 'text': text}}})
request('java/buildWorkspace', True)
# (implementations in the sources, whether they are all there are)
cases = {
# Classes, abstract ones too; not the sub-interface.
('Repo.java', 'Repo<T>'): ['Base.java:2', 'Numbers.java:2', 'Words.java:2'],
# Classes, abstract ones too, and those of the bridged Kotlin output; not the sub-interface.
('Repo.java', 'Repo<T>'): (['Base.java:2', 'Longs.java:0', 'Numbers.java:2', 'Words.java:2'], True),
# Through the type arguments; not the abstract re-declaration.
('Repo.java', 'save(T'): ['Numbers.java:3', 'Words.java:3'],
('Use.java', 'save("x")'): ['Numbers.java:3', 'Words.java:3'],
('Repo.java', 'name()'): ['Words.java:6'],
# A library type's implementations in the sources.
('Use.java', 'Runnable task'): ['Task.java:2'],
('Use.java', 'run();'): ['Task.java:3'],
('Use.java', 'alone()'): [],
('Repo.java', 'save(T'): (['Longs.java:0', 'Numbers.java:3', 'Words.java:3'], True),
('Use.java', 'save("x")'): (['Longs.java:0', 'Numbers.java:3', 'Words.java:3'], True),
('Repo.java', 'name()'): (['Words.java:6'], True),
# A library type's implementations in the sources; the library has more.
('Use.java', 'Runnable task'): (['Task.java:2'], False),
('Use.java', 'run();'): (['Task.java:3'], False),
('Use.java', 'alone()'): ([], True),
}
failures = []
for (file, needle), expected in cases.items():
+1 -1
View File
@@ -37,7 +37,7 @@ sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host)
crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave
```
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.1.0`, world
The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.2.0`, world
`plugin`).
## 1. Quick start
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
12c9f58ab05321fa3966a0179c085e2972db278adf20b917b50f30ce048aa67d
f884ceac7f40567a2e817d3b40830a7b0da2f9d5ffa519bf67d4ff667c0f7da0
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
69273e1ddfd2fff9f49c11d4fbdc60a771f8e3befe440e1623c2ab97469aa250
4c81f468df030724df26a6b57370d8ea4958a3cf8d3a40b23458ed3b2dd32505
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
74167252b0e306f94eb00ac9b467a5f30c373b9f15e223b21afa71c67c1dec0b
266430921ca7b2271f452c2e8bc4893bd5c1d1516f8081a4ab09950ede9a586b
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
66e7f43eaf7bbadf1c5c817c6ce8da3adca0543ce5e12550440b4039b235581d
91f3f03a81acd298b26c539003a96d0e87a88139a2ec6b9ade85e9ca2e89d1a1
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
04cd70714c6bac560783492b3bd31d22f7056b9d8809df14163be935785dd114
e7cdaeb4f482f698026542c1545f3a1f4734f78f7e9ca0a83cdbc805669c2c7c
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
674f439626f08e931842c0c108a57c899ea38d2b5b2de38debd3b4b46feb5777
ebdcbc36896ab0d545946a95befbc5aeb2b127d615c4754b0077975c53ee0286
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ee40771fe778d4af1f762c1512155ea375ee3ed238390c65a5461b077fed6f8b
786fd1808d786dd6abd2671b567db1e37075c08610ac4715c93c0624dbe396d6
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
b9cc6408738acd7a89ff76632d91ec49680c7a5ff6bee292efa77d182003ec25
500b934eafbe02493b8d2b42f779281edd00b899f70ed8732de3edc377ceca63
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
6c9c35547a7ce1c240be83bbb30418a28ab8e2e4034401205e35ec2dee87abbf
2ab43c67c74bd9f8d8ef788b8dca2f6f7288e79ad53663dc51624f36730e52f7
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
56af2a764333e739d53fb5c8346835b4796cf76611c36b4685a428b49cc4eae2
db60e5fccb72de283a772176052bf659a4ebff11bfb93f634dd513ec878837a7
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
ac745c20866ed959252778116af068d7264187a53df64e2de090522b7ba5b722
fa82a8596bd6505c769c0125ea26cef9af08535b36f650ddc4a353eb50866cea
+6 -6
View File
@@ -89,7 +89,7 @@ pub mod providers_bindings {
export_macro_name: "__export_editor_providers_world",
default_bindings_module: "openidea_plugin::providers_bindings",
with: {
"openidea:plugin/types@1.1.0": crate::bindings::openidea::plugin::types,
"openidea:plugin/types@1.2.0": crate::bindings::openidea::plugin::types,
},
});
}
@@ -106,16 +106,16 @@ pub mod views_bindings {
export_macro_name: "__export_views_world",
default_bindings_module: "openidea_plugin::views_bindings",
with: {
"openidea:plugin/types@1.1.0": crate::bindings::openidea::plugin::types,
"openidea:plugin/workspace@1.1.0": crate::bindings::openidea::plugin::workspace,
"openidea:plugin/editor@1.1.0": crate::bindings::openidea::plugin::editor,
"openidea:plugin/invocation@1.1.0": crate::bindings::openidea::plugin::invocation,
"openidea:plugin/types@1.2.0": crate::bindings::openidea::plugin::types,
"openidea:plugin/workspace@1.2.0": crate::bindings::openidea::plugin::workspace,
"openidea:plugin/editor@1.2.0": crate::bindings::openidea::plugin::editor,
"openidea:plugin/invocation@1.2.0": crate::bindings::openidea::plugin::invocation,
},
});
}
/// The plugin API version this SDK targets.
pub const API_VERSION: &str = "1.1";
pub const API_VERSION: &str = "1.2";
/// A plugin. One instance lives as long as the plugin's Wasm instance (until
/// it is disabled, crashes or is reclaimed when idle).
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Workbench events, delivered in batches to `event-handler.handle`.
interface events {
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
interface lifecycle {
use types.{context, error};
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Writes to the IDE log, tagged with the plugin id.
interface log {
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Problems in files, shown in the editor and the Problems view.
interface diagnostics {
+1 -1
View File
@@ -7,7 +7,7 @@
/// or processes beyond WASI's clocks and randomness; everything else goes
/// through the imports below, gated by the capabilities granted in
/// `plugin.toml` (a missing one returns `error::denied`).
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
world plugin {
import log;
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Editor features a plugin provides for the languages it declares under
/// `[[contributes.providers]]` (the host calls only declared kinds). They run
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Shared types. Positions are 0-based lines and UTF-16 code unit columns,
/// as in the editor and LSP.
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Declarative views: tool windows declared
/// under `[[contributes.views]]` and custom editors under
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
interface workspace {
use types.{error};
+17
View File
@@ -80,6 +80,23 @@ pub async fn lsp_goto(
ide_host::ops::lsp::lsp_goto(&state.host, window.label(), doc, pos, kind).await
}
/// Go to Implementation of the symbol at `pos`. Implementations found before a
/// slower search for the rest arrive on `on_partial`; the result has them all.
/// Cancel with `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn lsp_goto_implementation(
window: Window,
state: State<'_, AppState>,
request: RequestId,
doc: DocId,
pos: Pos,
on_partial: tauri::ipc::Channel<Vec<Location>>,
) -> IdeResult<Vec<Location>> {
let on_partial = super::sender(on_partial);
ide_host::ops::lsp::lsp_goto_implementation(&state.host, window.label(), request, doc, pos, on_partial).await
}
/// Implementations of the symbols at `positions`, one list per position (the
/// editor's "is implemented / overridden" gutter markers). Cancel with
/// `cancel_request(request)`.
+1
View File
@@ -219,6 +219,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
lsp::lsp_signature_help,
lsp::lsp_goto,
lsp::lsp_implementations,
lsp::lsp_goto_implementation,
lsp::lsp_virtual_document,
lsp::lsp_resolve_class_file,
lsp::lsp_references,
+6
View File
@@ -409,6 +409,12 @@ export const commands = {
* `cancel_request(request)`.
*/
lspImplementations: (request: RequestId, doc: DocId, positions: Pos[]) => typedError<Location[][], IdeError>(__TAURI_INVOKE("lsp_implementations", { request, doc, positions })),
/**
* Go to Implementation of the symbol at `pos`. Implementations found before a
* slower search for the rest arrive on `on_partial`; the result has them all.
* Cancel with `cancel_request(request)`.
*/
lspGotoImplementation: (request: RequestId, doc: DocId, pos: Pos, onPartial: Channel<Location[]>) => typedError<Location[], IdeError>(__TAURI_INVOKE("lsp_goto_implementation", { request, doc, pos, onPartial })),
/**
* Contents of a read-only language-server document, including JDT library
* classes and disk class files. Cached per project.
+102 -4
View File
@@ -15,13 +15,17 @@ import {
type Tooltip,
type ViewUpdate,
} from "@codemirror/view";
import { Channel } from "@tauri-apps/api/core";
import {
commands,
isCancelled,
nextRequestId,
type DocId,
type GotoKind,
type Highlight,
type Location,
type Pos,
type RequestId,
type WebReferenceKind,
} from "@/platform/ipc";
import {
@@ -48,7 +52,7 @@ import {
} from "./syntaxFallback";
import { showHint, showPendingHint } from "./hint";
import { outsideProjectReason } from "./outsideProject";
import { caretAnchor, openPopup, type ListPopupItem } from "./popupStore";
import { caretAnchor, openPopup, updatePopup, type ListPopupItem } from "./popupStore";
import { readManyFileLines, targetLabel } from "./files";
import {
documentLinks,
@@ -134,9 +138,89 @@ export async function locationItems(locs: readonly DeclarationTarget[]): Promise
/** "Choose Implementation of foo (3 found)". */
export function chooserTitle(kind: GotoKind, name: string | null, count: number, fromSyntax = false): string {
if (kind === "implementation" && !fromSyntax)
return name ? tx`Choose Implementation of ${name} (${count} found)` : tx`Choose Implementation (${count} found)`;
return `${TITLES[kind]}${name ? ` of ${name}` : ""} (${count} found${fromSyntax ? ", based on syntax" : ""})`;
}
/** The chooser's title while the server is still searching for more. */
function searchingTitle(name: string | null, count: number): string {
return name
? tx`Choose Implementation of ${name} (${count} found, searching…)`
: tx`Choose Implementation (${count} found, searching…)`;
}
function nameAt(view: EditorView, offset: number): string | null {
const word = view.state.wordAt(offset);
return word ? view.state.sliceDoc(word.from, word.to) : null;
}
/**
* Go to Implementation whose chooser opens with the implementations the server finds first
* (Java: the project's) while it searches on (the dependencies'), and fills in when it ends.
* Closing the chooser cancels the search.
*/
function streamedImplementations(view: EditorView, offset: number, onOpen: () => void) {
const name = nameAt(view, offset);
let current: RequestId | null = null;
let finished = false;
let opened = false;
let closed = false;
let first: DeclarationTarget[] = [];
let showing: Promise<void> = Promise.resolve();
return {
request(doc: DocId, pos: Pos): Promise<DeclarationTarget[]> {
const request = (current = nextRequestId());
finished = false;
const channel = new Channel<Location[]>();
channel.onmessage = (locs) => {
if (finished || opened || current !== request || !locs.length) return;
opened = true;
onOpen();
first = dedupeLocations(locs);
showing = locationItems(first).then((items) => {
// All of them arrived meanwhile: shown as usual.
if (finished) {
opened = false;
return;
}
openPopup({
kind: "list",
anchor: caretAnchor(view, offset),
title: searchingTitle(name, first.length),
items,
pin: pinToFind(name ?? "", first),
onClose: () => {
closed = true;
if (!finished) void commands.cancelRequest(request).catch(() => {});
},
});
});
};
return lspRequest(() => commands.lspGotoImplementation(request, doc, pos, channel)).finally(() => {
finished = true;
});
},
/** Whether the chooser opened with the first ones (and so the search's result goes there). */
async opened(): Promise<boolean> {
await showing;
return opened;
},
/** Fills the opened chooser with all of them, after those it shows. */
async complete(locs: readonly DeclarationTarget[]) {
if (closed) return;
const all = dedupeLocations([...first, ...locs]);
const items = await locationItems(all);
if (closed) return;
updatePopup("list", {
title: chooserTitle("implementation", name, all.length),
items,
pin: pinToFind(name ?? "", all),
});
},
};
}
/** Pin button of the targets chooser: the targets as a Find tool window tab (code-insight). */
export function pinToFind(symbol: string, locations: readonly Location[]) {
return {
@@ -175,14 +259,25 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
let fromSyntax = false;
const declaration = kind === "definition" || kind === "declaration";
let serverDown = false;
let streamed: ReturnType<typeof streamedImplementations> | null = null;
try {
const done = showPendingHint(view, tx`Finding navigation target…`);
const stream = kind === "implementation" ? streamedImplementations(view, offset, done) : null;
streamed = stream;
const request = stream
? () => stream.request(doc.doc, pos)
: () => lspRequest(() => commands.lspGoto(doc.doc, pos, kind));
try {
locs = dedupeLocations(await gotoAfterEdits(view, () => lspRequest(() => commands.lspGoto(doc.doc, pos, kind))));
locs = dedupeLocations(await gotoAfterEdits(view, request));
} finally {
done();
}
} catch (e) {
// The chooser shows the first ones: closing it cancelled the search, or the rest failed.
if (streamed && (await streamed.opened())) {
await streamed.complete([]);
return;
}
if (isCancelled(e) || !declaration) {
reportLspError(view, e, true);
return;
@@ -199,6 +294,10 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
locs = fb;
fromSyntax = true;
}
if (streamed && (await streamed.opened())) {
await streamed.complete(locs);
return;
}
if (!locs.length) {
// The server has no definition here: a link (`href`, `#include`, Markdown) or a file path.
if (
@@ -234,8 +333,7 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
}, 150);
return;
}
const word = view.state.wordAt(offset);
const name = word ? view.state.sliceDoc(word.from, word.to) : null;
const name = nameAt(view, offset);
openPopup({
kind: "list",
anchor: caretAnchor(view, offset),
+10 -1
View File
@@ -10,8 +10,17 @@ import { registerAction } from "@/platform/actions";
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
vi.mock("@/platform/ipc", async (orig) => {
const real = await orig<typeof import("@/platform/ipc")>();
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto } };
// Go to Implementation streams (`lspGotoImplementation`); here it answers all at once.
const lspGotoImplementation: typeof commands.lspGotoImplementation = (_request, doc, pos) =>
lspGoto(doc, pos, "implementation");
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto, lspGotoImplementation } };
});
vi.mock("@tauri-apps/api/core", async (orig) => ({
...(await orig<object>()),
Channel: class {
onmessage = () => {};
},
}));
const { gotoTargets, navigationExtension } = await import("./navigation");
const { lspDoc } = await import("./lsp");
+99 -6
View File
@@ -1,16 +1,35 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import type { commands } from "@/platform/ipc";
import type { commands, Location } from "@/platform/ipc";
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
const { lspGoto, lspGotoImplementation, cancelRequest } = vi.hoisted(() => ({
lspGoto: vi.fn<typeof commands.lspGoto>(),
lspGotoImplementation: vi.fn<typeof commands.lspGotoImplementation>(),
cancelRequest: vi.fn<typeof commands.cancelRequest>(async () => {}),
}));
vi.mock("@/platform/ipc", async (orig) => {
const real = await orig<typeof import("@/platform/ipc")>();
return { ...real, isTauri: () => true, commands: { ...real.commands, lspGoto } };
const fsReadText: typeof commands.fsReadText = async () => ({
status: "error",
error: { kind: "notFound", message: "not found" },
});
return {
...real,
isTauri: () => true,
commands: { ...real.commands, lspGoto, lspGotoImplementation, cancelRequest, fsReadText },
};
});
vi.mock("@tauri-apps/api/core", async (orig) => ({
...(await orig<object>()),
Channel: class {
onmessage = () => {};
},
}));
const { gotoTargets } = await import("./navigation");
const { lspDoc } = await import("./lsp");
const { useLspPopup, closePopup } = await import("./popupStore");
const { provideEditorService } = await import("@/workbench/contributions");
const openFile = vi.fn(async () => {});
provideEditorService({ openFile } as never);
@@ -50,13 +69,87 @@ describe("slow navigation feedback", () => {
});
it("replaces progress with the server error", async () => {
let resolve!: (result: Awaited<ReturnType<typeof commands.lspGoto>>) => void;
lspGoto.mockImplementationOnce(() => new Promise((r) => (resolve = r)));
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.advanceTimersByTimeAsync(250);
resolve({ status: "error", error: { kind: "protocol", message: "Server timed out" } });
search.resolve({ status: "error", error: { kind: "protocol", message: "Server timed out" } });
await pending;
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe("Server timed out");
expect(openFile).not.toHaveBeenCalled();
});
});
const at = (path: string, line: number): Location => ({
path,
range: { start: { line, col: 0 }, end: { line, col: 1 } },
uri: null,
});
/** A Go to Implementation request answered when the test says: the first ones, then all. */
function slowImplementations() {
let channel!: { onmessage: (locs: Location[]) => void };
let resolve!: (result: Awaited<ReturnType<typeof commands.lspGotoImplementation>>) => void;
lspGotoImplementation.mockImplementationOnce((_request, _doc, _pos, onPartial) => {
channel = onPartial as unknown as typeof channel;
return new Promise((r) => (resolve = r));
});
return {
first: (locs: Location[]) => channel.onmessage(locs),
resolve: (r: Parameters<typeof resolve>[0]) => resolve(r),
};
}
const listPopup = () => {
const popup = useLspPopup.getState().popup;
return popup?.kind === "list" ? popup : null;
};
describe("Go to Implementation while the search goes on", () => {
afterEach(() => {
closePopup();
cancelRequest.mockClear();
lspGotoImplementation.mockReset();
});
it("opens the chooser with the implementations found first, then adds the rest", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.advanceTimersByTimeAsync(250);
search.first([at("/p/Own.java", 3)]);
await vi.waitFor(() => expect(listPopup()?.title).toBe("Choose Implementation of run (1 found, searching…)"));
expect(listPopup()!.items.map((i) => i.label)).toEqual(["Own.java"]);
expect(document.querySelector(".cm-lsp-hint")).toBeNull();
search.resolve({ status: "ok", data: [at("/p/Library.java", 8), at("/p/Own.java", 3)] });
await pending;
expect(listPopup()?.title).toBe("Choose Implementation of run (2 found)");
expect(listPopup()!.items.map((i) => i.label)).toEqual(["Own.java", "Library.java"]);
expect(openFile).not.toHaveBeenCalled();
expect(cancelRequest).not.toHaveBeenCalled();
});
it("cancels the search when the chooser closes", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.waitFor(() => expect(lspGotoImplementation).toHaveBeenCalled());
search.first([at("/p/Own.java", 3)]);
await vi.waitFor(() => expect(listPopup()).not.toBeNull());
closePopup();
expect(cancelRequest).toHaveBeenCalledExactlyOnceWith(lspGotoImplementation.mock.lastCall![0]);
search.resolve({ status: "error", error: { kind: "cancelled", message: "cancelled" } });
await pending;
expect(listPopup()).toBeNull();
expect(document.querySelector(".cm-lsp-hint")).toBeNull();
});
it("goes to the only implementation when none came first", async () => {
const search = slowImplementations();
const pending = gotoTargets(view, "implementation");
await vi.waitFor(() => expect(lspGotoImplementation).toHaveBeenCalled());
search.resolve({ status: "ok", data: [at("/p/Library.java", 8)] });
await pending;
expect(listPopup()).toBeNull();
expect(openFile).toHaveBeenCalledWith("/p/Library.java", { selection: { line: 8, col: 0 }, focus: true });
});
});
+3
View File
@@ -4048,6 +4048,9 @@
"Choose Super Class or Interface ({count} found)": "选择超类或接口(找到 {count} 个)",
"Choose Super Method of {name} ({count} found)": "选择 {name} 的超方法(找到 {count} 个)",
"Choose Implementation of {name} ({count} found)": "选择 {name} 的实现(找到 {count} 个)",
"Choose Implementation ({count} found)": "选择实现(找到 {count} 个)",
"Choose Implementation of {name} ({count} found, searching…)": "选择 {name} 的实现(已找到 {count} 个,仍在搜索…)",
"Choose Implementation ({count} found, searching…)": "选择实现(已找到 {count} 个,仍在搜索…)",
"Choose Subclass of {name} ({count} found)": "选择 {name} 的子类(找到 {count} 个)",
"Choose Overriding Method of {name} ({count} found)": "选择 {name} 的重写方法(找到 {count} 个)",
"Is implemented by {names}": "由 {names} 实现",
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Workbench events, delivered in batches to `event-handler.handle`.
interface events {
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
interface lifecycle {
use types.{context, error};
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Writes to the IDE log, tagged with the plugin id.
interface log {
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Problems in files, shown in the editor and the Problems view.
interface diagnostics {
+1 -1
View File
@@ -7,7 +7,7 @@
/// or processes beyond WASI's clocks and randomness; everything else goes
/// through the imports below, gated by the capabilities granted in
/// `plugin.toml` (a missing one returns `error::denied`).
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
world plugin {
import log;
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Editor features a plugin provides for the languages it declares under
/// `[[contributes.providers]]` (the host calls only declared kinds). They run
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Shared types. Positions are 0-based lines and UTF-16 code unit columns,
/// as in the editor and LSP.
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
/// Declarative views: tool windows declared
/// under `[[contributes.views]]` and custom editors under
+1 -1
View File
@@ -1,4 +1,4 @@
package openidea:plugin@1.1.0;
package openidea:plugin@1.2.0;
interface workspace {
use types.{error};