mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 18:38:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb258b0912 |
No files matched your search
@@ -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
@@ -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();
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
@@ -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)`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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} 实现",
|
||||
|
||||
Reference in new issue
Block a user