mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-11 23:56:12 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb258b0912 |
No files matched your search
@@ -257,6 +257,28 @@ pub async fn lsp_implementations(
|
|||||||
result
|
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
|
/// Contents of a read-only language-server document, including JDT library
|
||||||
/// classes and disk class files. Cached per project.
|
/// classes and disk class files. Cached per project.
|
||||||
pub async fn lsp_virtual_document(host: &Host, window: &str, uri: String) -> IdeResult<VirtualDocument> {
|
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 {
|
d.add("lsp_implementations", |c| async move {
|
||||||
lsp_implementations(&c.host, &c.window, c.arg("request")?, c.arg("doc")?, c.arg("positions")?).await
|
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(
|
d.add(
|
||||||
"lsp_resolve_class_file",
|
"lsp_resolve_class_file",
|
||||||
|c| async move { lsp_resolve_class_file(&c.host, &c.window, c.arg("path")?).await },
|
|c| async move { lsp_resolve_class_file(&c.host, &c.window, c.arg("path")?).await },
|
||||||
|
|||||||
+71
-15
@@ -317,6 +317,25 @@ async fn library_sources(snap: &DocSnap, pos: Pos, locs: Vec<Location>) -> Vec<L
|
|||||||
|
|
||||||
const JAVA_IMPLEMENTATIONS: &str = "openidea.java.implementations";
|
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) {
|
fn goto_method(kind: GotoKind) -> (&'static str, &'static str, &'static str) {
|
||||||
match kind {
|
match kind {
|
||||||
GotoKind::Definition => ("textDocument/definition", "definitionProvider", "go to definition"),
|
GotoKind::Definition => ("textDocument/definition", "definitionProvider", "go to definition"),
|
||||||
@@ -1231,7 +1250,22 @@ impl LspManager {
|
|||||||
}
|
}
|
||||||
let (method, cap, feature) = goto_method(kind);
|
let (method, cap, feature) = goto_method(kind);
|
||||||
let (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
|
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
|
/// 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 (snap, targets) = self.doc_targets(doc, feature, |c| c.has(cap)).await?;
|
||||||
let requests = positions.iter().copied().map(|pos| {
|
let requests = positions.iter().copied().map(|pos| {
|
||||||
let (snap, targets, cancel) = (&snap, &targets, &cancel);
|
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>>> =
|
let results: Vec<IdeResult<Vec<Location>>> =
|
||||||
futures::stream::iter(requests).buffered(IMPLEMENTATION_CONCURRENCY).collect().await;
|
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())
|
Ok(results.into_iter().map(Result::unwrap_or_default).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// jdtls: the implementations in the project's sources (`openidea.java.implementations`).
|
/// 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
|
/// and whether they are all there are. Its standard request builds the type hierarchy over
|
||||||
/// long in a project with many dependencies; empty (or no extension) falls back to it.
|
/// every dependency jar too, which takes long in a project with many dependencies.
|
||||||
async fn java_source_implementations(
|
async fn java_source_implementations(
|
||||||
&self,
|
&self,
|
||||||
snap: &DocSnap,
|
snap: &DocSnap,
|
||||||
t: &Target,
|
t: &Target,
|
||||||
pos: Pos,
|
pos: Pos,
|
||||||
cancel: Option<&CancellationToken>,
|
cancel: Option<&CancellationToken>,
|
||||||
) -> Vec<Location> {
|
) -> (Vec<Location>, bool) {
|
||||||
if t.server.name != "jdtls" || !t.caps().commands().iter().any(|c| c == JAVA_IMPLEMENTATIONS) {
|
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 params = snap.pos_params(t, pos);
|
||||||
let arguments =
|
let arguments =
|
||||||
json!([params["textDocument"]["uri"], params["position"]["line"], params["position"]["character"]]);
|
json!([params["textDocument"]["uri"], params["position"]["line"], params["position"]["character"]]);
|
||||||
let result: IdeResult<Vec<lsp::Location>> = self
|
let result: IdeResult<SourceImplementations> = self
|
||||||
.call(
|
.call(
|
||||||
t,
|
t,
|
||||||
"workspace/executeCommand",
|
"workspace/executeCommand",
|
||||||
@@ -1287,10 +1321,16 @@ impl LspManager {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
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) => {
|
Err(e) => {
|
||||||
tracing::debug!("{JAVA_IMPLEMENTATIONS}: {}", e.message);
|
tracing::debug!("{JAVA_IMPLEMENTATIONS}: {}", e.message);
|
||||||
Vec::new()
|
(Vec::new(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1302,16 +1342,23 @@ impl LspManager {
|
|||||||
method: &str,
|
method: &str,
|
||||||
pos: Pos,
|
pos: Pos,
|
||||||
cancel: Option<&CancellationToken>,
|
cancel: Option<&CancellationToken>,
|
||||||
|
partial: Partial<'_>,
|
||||||
) -> IdeResult<Vec<Location>> {
|
) -> IdeResult<Vec<Location>> {
|
||||||
let mut out = Outcome::new();
|
let mut out = Outcome::new();
|
||||||
for t in targets.iter().filter(|t| snap.asks(t, pos)) {
|
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" {
|
if method == "textDocument/implementation" {
|
||||||
let locs = self.java_source_implementations(snap, t, pos, cancel).await;
|
let (locs, complete) = self.java_source_implementations(snap, t, pos, cancel).await;
|
||||||
if !locs.is_empty() {
|
if complete && !locs.is_empty() {
|
||||||
let locs = library_sources(snap, pos, locs).await;
|
|
||||||
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
|
|
||||||
return Ok(locs);
|
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, || {
|
let r = loading::while_loading(&t.server, cancel, loading::LOADING_WAIT, goto_is_empty, || {
|
||||||
self.call::<_, Option<lsp::GotoDefinitionResponse>>(
|
self.call::<_, Option<lsp::GotoDefinitionResponse>>(
|
||||||
@@ -1328,12 +1375,19 @@ impl LspManager {
|
|||||||
let locs = convert::goto_response(&self.conv(t.enc()), r);
|
let locs = convert::goto_response(&self.conv(t.enc()), r);
|
||||||
let locs = library_sources(snap, pos, locs).await;
|
let locs = library_sources(snap, pos, locs).await;
|
||||||
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
|
self.note_virtual(&t.server, locs.iter().map(|l| l.uri.as_deref()));
|
||||||
|
let locs = merge_locations(first, locs);
|
||||||
if !locs.is_empty() {
|
if !locs.is_empty() {
|
||||||
return Ok(locs);
|
return Ok(locs);
|
||||||
}
|
}
|
||||||
out.value = Some(Vec::new());
|
out.value = Some(Vec::new());
|
||||||
}
|
}
|
||||||
|
Ok(None) if !first.is_empty() => return Ok(first),
|
||||||
Ok(None) => out.value = Some(Vec::new()),
|
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),
|
Err(e) => out.fail(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1517,7 +1571,9 @@ impl LspManager {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let defining = self.ready_targets(&snap.servers, |c| c.has("definitionProvider")).await;
|
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 (results, declarations) = futures::join!(join_all(requests), declarations);
|
||||||
let mut out = Outcome::new();
|
let mut out = Outcome::new();
|
||||||
|
|||||||
@@ -1816,23 +1816,57 @@ async fn java_implementations_come_from_sources_first() {
|
|||||||
h.wait_status("jdtls", ServerStatus::Ready).await;
|
h.wait_status("jdtls", ServerStatus::Ready).await;
|
||||||
let s = h.launcher.state("jdtls");
|
let s = h.launcher.state("jdtls");
|
||||||
let uri = uri_of(&path);
|
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 {
|
*s.responder.lock() = Some(Arc::new(move |_, method, params| match method {
|
||||||
// The sources have an implementation of line 0 only.
|
// Line 0 is declared in the sources; line 2 in a dependency, which implements it on line 3.
|
||||||
"workspace/executeCommand" => Some(if params["arguments"][1] == 0 { at(1) } else { json!([]) }),
|
"workspace/executeCommand" => Some(match params["arguments"][1].as_u64() {
|
||||||
"textDocument/implementation" => Some(at(3)),
|
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,
|
_ => 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();
|
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");
|
assert!(s.requested("textDocument/implementation").is_empty(), "no search through the dependencies");
|
||||||
let asked = s.requested("workspace/executeCommand");
|
let asked = s.requested("workspace/executeCommand");
|
||||||
assert_eq!(asked[0]["command"], "openidea.java.implementations");
|
assert_eq!(asked[0]["command"], "openidea.java.implementations");
|
||||||
assert_eq!((asked[0]["arguments"][1].clone(), asked[0]["arguments"][2].clone()), (json!(0), json!(10)));
|
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();
|
// A dependency's type: those in the sources first, then the dependencies' too.
|
||||||
assert_eq!(found.iter().map(|l| l.range.start.line).collect::<Vec<_>>(), vec![3]);
|
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);
|
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")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
|||||||
@@ -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.
|
/// 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> {
|
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");
|
let bytes = include_bytes!("../../../java/java-usages/openidea-java-usages.jar");
|
||||||
if std::fs::read(&extension).is_ok_and(|old| old == bytes) {
|
if std::fs::read(&extension).is_ok_and(|old| old == bytes) {
|
||||||
clean_usages_bundles(data_dir, &extension);
|
clean_usages_bundles(data_dir, &extension);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
|
|||||||
Bundle-ManifestVersion: 2
|
Bundle-ManifestVersion: 2
|
||||||
Bundle-Name: OpenIDEA Java usages
|
Bundle-Name: OpenIDEA Java usages
|
||||||
Bundle-SymbolicName: ai.openidea.usages;singleton:=true
|
Bundle-SymbolicName: ai.openidea.usages;singleton:=true
|
||||||
Bundle-Version: 1.4.0
|
Bundle-Version: 1.5.0
|
||||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||||
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
|
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
|
||||||
org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime,
|
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
|
## Rebuild and delivery
|
||||||
|
|
||||||
The checked-in jar is embedded by ide-sdk `include_bytes!`, installed as
|
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
|
Users need no compiler, Maven or Python. Manifest/install version must change
|
||||||
when replacing the bundle because OSGi caches bundles.
|
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
|
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
|
source folders, before the standard request that also searches every dependency jar); its
|
||||||
reproducible SHA-256 is `d7601493eda846811b4bb8bfe7f3d56e3234a45e663648210db3937cebc770a5`.
|
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
|
```sh
|
||||||
export PATH=/path/to/jdk25/bin:$PATH
|
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
|
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
|
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/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
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.eclipse.core.resources.ResourcesPlugin;
|
import org.eclipse.core.resources.ResourcesPlugin;
|
||||||
import org.eclipse.core.runtime.IProgressMonitor;
|
import org.eclipse.core.runtime.IProgressMonitor;
|
||||||
import org.eclipse.core.runtime.OperationCanceledException;
|
import org.eclipse.core.runtime.OperationCanceledException;
|
||||||
@@ -13,39 +14,52 @@ import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
|
|||||||
import org.eclipse.lsp4j.Location;
|
import org.eclipse.lsp4j.Location;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementations of the type or method at a position, looked up in the workspace's source
|
* Implementations of the type or method at a position, looked up in the workspace's own code
|
||||||
* folders only. The standard request builds the type hierarchy over every dependency jar too,
|
* only (source folders and bridged Kotlin classes): {@code {locations, complete}}. The standard
|
||||||
* which takes long in a project with many dependencies; the client falls back to it when the
|
* request builds the type hierarchy over every dependency jar too, which takes long in a project
|
||||||
* sources have none.
|
* 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 {
|
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
|
@Override
|
||||||
public Object executeCommand(String command, List<Object> arguments, IProgressMonitor monitor) throws Exception {
|
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);
|
String uri = (String) arguments.get(0);
|
||||||
int line = ((Number) arguments.get(1)).intValue();
|
int line = ((Number) arguments.get(1)).intValue();
|
||||||
int column = ((Number) arguments.get(2)).intValue();
|
int column = ((Number) arguments.get(2)).intValue();
|
||||||
List<Location> result = new ArrayList<>();
|
|
||||||
ITypeRoot root = JDTUtils.resolveTypeRoot(uri);
|
ITypeRoot root = JDTUtils.resolveTypeRoot(uri);
|
||||||
if (root == null) return result;
|
if (root == null) return false;
|
||||||
var preferences = JavaLanguageServerPlugin.getPreferencesManager();
|
var preferences = JavaLanguageServerPlugin.getPreferencesManager();
|
||||||
IJavaElement element = JDTUtils.findElementAtSelection(root, line, column, preferences, monitor);
|
IJavaElement element = JDTUtils.findElementAtSelection(root, line, column, preferences, monitor);
|
||||||
IMethod method = null;
|
IMethod method = null;
|
||||||
IType type;
|
IType type;
|
||||||
if (element instanceof IMethod m) {
|
if (element instanceof IMethod m) {
|
||||||
int flags = m.getFlags();
|
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;
|
method = m;
|
||||||
type = m.getDeclaringType();
|
type = m.getDeclaringType();
|
||||||
} else if (element instanceof IType t) {
|
} else if (element instanceof IType t) {
|
||||||
type = t;
|
type = t;
|
||||||
} else {
|
} 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();
|
IRegion sources = JavaCore.newRegion();
|
||||||
for (IJavaProject project : JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()).getJavaProjects()) {
|
for (IJavaProject project : JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()).getJavaProjects()) {
|
||||||
for (IPackageFragmentRoot fragmentRoot : project.getPackageFragmentRoots()) {
|
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);
|
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);
|
Location location = found == null ? null : JDTUtils.toLocation(found);
|
||||||
if (location != null) result.add(location);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,8 @@ config_source = next(home / name / 'config.ini' for name in config_names if (hom
|
|||||||
project = work / 'project'
|
project = work / 'project'
|
||||||
(project / 'src/zoo').mkdir(parents=True)
|
(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 / '.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 = {
|
sources = {
|
||||||
'Repo.java': '''package zoo;
|
'Repo.java': '''package zoo;
|
||||||
|
|
||||||
@@ -88,6 +89,10 @@ public class Use {
|
|||||||
for name, text in sources.items():
|
for name, text in sources.items():
|
||||||
(project / 'src/zoo' / name).write_text(text)
|
(project / 'src/zoo' / name).write_text(text)
|
||||||
uri = lambda name: (project / 'src/zoo' / name).as_uri()
|
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')
|
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)
|
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)
|
line, col = position(file, needle)
|
||||||
found = command('openidea.java.implementations', uri(file), line, col)
|
found = command('openidea.java.implementations', uri(file), line, col)
|
||||||
names = []
|
names = []
|
||||||
for loc in found:
|
for loc in found['locations']:
|
||||||
name = pathlib.Path(urllib.parse.unquote(urllib.parse.urlparse(loc['uri']).path)).name
|
name = pathlib.Path(urllib.parse.unquote(urllib.parse.urlparse(loc['uri']).path)).name
|
||||||
names.append(f"{name}:{loc['range']['start']['line']}")
|
names.append(f"{name}:{loc['range']['start']['line']}")
|
||||||
return sorted(names)
|
return sorted(names), found['complete']
|
||||||
|
|
||||||
|
|
||||||
try:
|
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']
|
assert 'openidea.java.implementations' in init['capabilities']['executeCommandProvider']['commands']
|
||||||
send({'jsonrpc': '2.0', 'method': 'initialized', 'params': {}})
|
send({'jsonrpc': '2.0', 'method': 'initialized', 'params': {}})
|
||||||
for name, text in sources.items():
|
for name, text in sources.items():
|
||||||
send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': uri(name), 'languageId': 'java', 'version': 1, 'text': text}}})
|
send({'jsonrpc': '2.0', 'method': 'textDocument/didOpen', 'params': {'textDocument': {'uri': uri(name), 'languageId': 'java', 'version': 1, 'text': text}}})
|
||||||
request('java/buildWorkspace', True)
|
request('java/buildWorkspace', True)
|
||||||
|
# (implementations in the sources, whether they are all there are)
|
||||||
cases = {
|
cases = {
|
||||||
# Classes, abstract ones too; not the sub-interface.
|
# Classes, abstract ones too, and those of the bridged Kotlin output; not the sub-interface.
|
||||||
('Repo.java', 'Repo<T>'): ['Base.java:2', 'Numbers.java:2', 'Words.java:2'],
|
('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.
|
# Through the type arguments; not the abstract re-declaration.
|
||||||
('Repo.java', 'save(T'): ['Numbers.java:3', 'Words.java:3'],
|
('Repo.java', 'save(T'): (['Longs.java:0', 'Numbers.java:3', 'Words.java:3'], True),
|
||||||
('Use.java', 'save("x")'): ['Numbers.java:3', 'Words.java:3'],
|
('Use.java', 'save("x")'): (['Longs.java:0', 'Numbers.java:3', 'Words.java:3'], True),
|
||||||
('Repo.java', 'name()'): ['Words.java:6'],
|
('Repo.java', 'name()'): (['Words.java:6'], True),
|
||||||
# A library type's implementations in the sources.
|
# A library type's implementations in the sources; the library has more.
|
||||||
('Use.java', 'Runnable task'): ['Task.java:2'],
|
('Use.java', 'Runnable task'): (['Task.java:2'], False),
|
||||||
('Use.java', 'run();'): ['Task.java:3'],
|
('Use.java', 'run();'): (['Task.java:3'], False),
|
||||||
('Use.java', 'alone()'): [],
|
('Use.java', 'alone()'): ([], True),
|
||||||
}
|
}
|
||||||
failures = []
|
failures = []
|
||||||
for (file, needle), expected in cases.items():
|
for (file, needle), expected in cases.items():
|
||||||
|
|||||||
@@ -80,6 +80,23 @@ pub async fn lsp_goto(
|
|||||||
ide_host::ops::lsp::lsp_goto(&state.host, window.label(), doc, pos, kind).await
|
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
|
/// Implementations of the symbols at `positions`, one list per position (the
|
||||||
/// editor's "is implemented / overridden" gutter markers). Cancel with
|
/// editor's "is implemented / overridden" gutter markers). Cancel with
|
||||||
/// `cancel_request(request)`.
|
/// `cancel_request(request)`.
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
|||||||
lsp::lsp_signature_help,
|
lsp::lsp_signature_help,
|
||||||
lsp::lsp_goto,
|
lsp::lsp_goto,
|
||||||
lsp::lsp_implementations,
|
lsp::lsp_implementations,
|
||||||
|
lsp::lsp_goto_implementation,
|
||||||
lsp::lsp_virtual_document,
|
lsp::lsp_virtual_document,
|
||||||
lsp::lsp_resolve_class_file,
|
lsp::lsp_resolve_class_file,
|
||||||
lsp::lsp_references,
|
lsp::lsp_references,
|
||||||
|
|||||||
@@ -409,6 +409,12 @@ export const commands = {
|
|||||||
* `cancel_request(request)`.
|
* `cancel_request(request)`.
|
||||||
*/
|
*/
|
||||||
lspImplementations: (request: RequestId, doc: DocId, positions: Pos[]) => typedError<Location[][], IdeError>(__TAURI_INVOKE("lsp_implementations", { request, doc, positions })),
|
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
|
* Contents of a read-only language-server document, including JDT library
|
||||||
* classes and disk class files. Cached per project.
|
* classes and disk class files. Cached per project.
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ import {
|
|||||||
type Tooltip,
|
type Tooltip,
|
||||||
type ViewUpdate,
|
type ViewUpdate,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
|
import { Channel } from "@tauri-apps/api/core";
|
||||||
import {
|
import {
|
||||||
commands,
|
commands,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
|
nextRequestId,
|
||||||
|
type DocId,
|
||||||
type GotoKind,
|
type GotoKind,
|
||||||
type Highlight,
|
type Highlight,
|
||||||
type Location,
|
type Location,
|
||||||
type Pos,
|
type Pos,
|
||||||
|
type RequestId,
|
||||||
type WebReferenceKind,
|
type WebReferenceKind,
|
||||||
} from "@/platform/ipc";
|
} from "@/platform/ipc";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +52,7 @@ import {
|
|||||||
} from "./syntaxFallback";
|
} from "./syntaxFallback";
|
||||||
import { showHint, showPendingHint } from "./hint";
|
import { showHint, showPendingHint } from "./hint";
|
||||||
import { outsideProjectReason } from "./outsideProject";
|
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 { readManyFileLines, targetLabel } from "./files";
|
||||||
import {
|
import {
|
||||||
documentLinks,
|
documentLinks,
|
||||||
@@ -134,9 +138,89 @@ export async function locationItems(locs: readonly DeclarationTarget[]): Promise
|
|||||||
|
|
||||||
/** "Choose Implementation of foo (3 found)". */
|
/** "Choose Implementation of foo (3 found)". */
|
||||||
export function chooserTitle(kind: GotoKind, name: string | null, count: number, fromSyntax = false): string {
|
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" : ""})`;
|
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). */
|
/** Pin button of the targets chooser: the targets as a Find tool window tab (code-insight). */
|
||||||
export function pinToFind(symbol: string, locations: readonly Location[]) {
|
export function pinToFind(symbol: string, locations: readonly Location[]) {
|
||||||
return {
|
return {
|
||||||
@@ -175,14 +259,25 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
|
|||||||
let fromSyntax = false;
|
let fromSyntax = false;
|
||||||
const declaration = kind === "definition" || kind === "declaration";
|
const declaration = kind === "definition" || kind === "declaration";
|
||||||
let serverDown = false;
|
let serverDown = false;
|
||||||
|
let streamed: ReturnType<typeof streamedImplementations> | null = null;
|
||||||
try {
|
try {
|
||||||
const done = showPendingHint(view, tx`Finding navigation target…`);
|
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 {
|
try {
|
||||||
locs = dedupeLocations(await gotoAfterEdits(view, () => lspRequest(() => commands.lspGoto(doc.doc, pos, kind))));
|
locs = dedupeLocations(await gotoAfterEdits(view, request));
|
||||||
} finally {
|
} finally {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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) {
|
if (isCancelled(e) || !declaration) {
|
||||||
reportLspError(view, e, true);
|
reportLspError(view, e, true);
|
||||||
return;
|
return;
|
||||||
@@ -199,6 +294,10 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
|
|||||||
locs = fb;
|
locs = fb;
|
||||||
fromSyntax = true;
|
fromSyntax = true;
|
||||||
}
|
}
|
||||||
|
if (streamed && (await streamed.opened())) {
|
||||||
|
await streamed.complete(locs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!locs.length) {
|
if (!locs.length) {
|
||||||
// The server has no definition here: a link (`href`, `#include`, Markdown) or a file path.
|
// The server has no definition here: a link (`href`, `#include`, Markdown) or a file path.
|
||||||
if (
|
if (
|
||||||
@@ -234,8 +333,7 @@ export async function gotoTargets(view: EditorView, kind: GotoKind, at?: number)
|
|||||||
}, 150);
|
}, 150);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const word = view.state.wordAt(offset);
|
const name = nameAt(view, offset);
|
||||||
const name = word ? view.state.sliceDoc(word.from, word.to) : null;
|
|
||||||
openPopup({
|
openPopup({
|
||||||
kind: "list",
|
kind: "list",
|
||||||
anchor: caretAnchor(view, offset),
|
anchor: caretAnchor(view, offset),
|
||||||
|
|||||||
@@ -10,8 +10,17 @@ import { registerAction } from "@/platform/actions";
|
|||||||
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
|
const lspGoto = vi.hoisted(() => vi.fn<typeof commands.lspGoto>());
|
||||||
vi.mock("@/platform/ipc", async (orig) => {
|
vi.mock("@/platform/ipc", async (orig) => {
|
||||||
const real = await orig<typeof import("@/platform/ipc")>();
|
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 { gotoTargets, navigationExtension } = await import("./navigation");
|
||||||
const { lspDoc } = await import("./lsp");
|
const { lspDoc } = await import("./lsp");
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { EditorState } from "@codemirror/state";
|
import { EditorState } from "@codemirror/state";
|
||||||
import { EditorView } from "@codemirror/view";
|
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) => {
|
vi.mock("@/platform/ipc", async (orig) => {
|
||||||
const real = await orig<typeof import("@/platform/ipc")>();
|
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 { gotoTargets } = await import("./navigation");
|
||||||
const { lspDoc } = await import("./lsp");
|
const { lspDoc } = await import("./lsp");
|
||||||
|
const { useLspPopup, closePopup } = await import("./popupStore");
|
||||||
const { provideEditorService } = await import("@/workbench/contributions");
|
const { provideEditorService } = await import("@/workbench/contributions");
|
||||||
const openFile = vi.fn(async () => {});
|
const openFile = vi.fn(async () => {});
|
||||||
provideEditorService({ openFile } as never);
|
provideEditorService({ openFile } as never);
|
||||||
@@ -50,13 +69,87 @@ describe("slow navigation feedback", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("replaces progress with the server error", async () => {
|
it("replaces progress with the server error", async () => {
|
||||||
let resolve!: (result: Awaited<ReturnType<typeof commands.lspGoto>>) => void;
|
const search = slowImplementations();
|
||||||
lspGoto.mockImplementationOnce(() => new Promise((r) => (resolve = r)));
|
|
||||||
const pending = gotoTargets(view, "implementation");
|
const pending = gotoTargets(view, "implementation");
|
||||||
await vi.advanceTimersByTimeAsync(250);
|
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;
|
await pending;
|
||||||
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe("Server timed out");
|
expect(document.querySelector(".cm-lsp-hint")?.textContent).toBe("Server timed out");
|
||||||
expect(openFile).not.toHaveBeenCalled();
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4048,6 +4048,9 @@
|
|||||||
"Choose Super Class or Interface ({count} found)": "选择超类或接口(找到 {count} 个)",
|
"Choose Super Class or Interface ({count} found)": "选择超类或接口(找到 {count} 个)",
|
||||||
"Choose Super Method of {name} ({count} found)": "选择 {name} 的超方法(找到 {count} 个)",
|
"Choose Super Method of {name} ({count} found)": "选择 {name} 的超方法(找到 {count} 个)",
|
||||||
"Choose Implementation 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 Subclass of {name} ({count} found)": "选择 {name} 的子类(找到 {count} 个)",
|
||||||
"Choose Overriding Method of {name} ({count} found)": "选择 {name} 的重写方法(找到 {count} 个)",
|
"Choose Overriding Method of {name} ({count} found)": "选择 {name} 的重写方法(找到 {count} 个)",
|
||||||
"Is implemented by {names}": "由 {names} 实现",
|
"Is implemented by {names}": "由 {names} 实现",
|
||||||
|
|||||||
Reference in new issue
Block a user