mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:08:16 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
352e8f718d | ||
|
|
c5951ac5d5 | ||
|
|
293b41dd53 |
No files matched your search
@@ -162,7 +162,8 @@ pub fn client_capabilities() -> Value {
|
||||
"callHierarchy": { "dynamicRegistration": false },
|
||||
"typeHierarchy": { "dynamicRegistration": false }
|
||||
},
|
||||
"experimental": {}
|
||||
// rust-analyzer: report when the project has loaded (`quiescent`).
|
||||
"experimental": { "serverStatusNotification": true }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ impl Handler for ClientHandler {
|
||||
let Some(mgr) = self.mgr.upgrade() else { return };
|
||||
match method {
|
||||
"$/progress" => self.progress(&mgr, params),
|
||||
"experimental/serverStatus" => {
|
||||
if let Some(quiescent) = params.get("quiescent").and_then(Value::as_bool) {
|
||||
self.server.set_loading(mgr.sink(), !quiescent);
|
||||
}
|
||||
}
|
||||
"textDocument/publishDiagnostics" => self.publish_diagnostics(&mgr, params),
|
||||
"window/showMessage" => self.show_message(&mgr, ¶ms),
|
||||
"window/logMessage" => {
|
||||
|
||||
@@ -18,6 +18,7 @@ mod inspect;
|
||||
pub mod jdtls;
|
||||
mod launch;
|
||||
mod library_source;
|
||||
mod loading;
|
||||
mod pull;
|
||||
mod scope;
|
||||
mod server;
|
||||
@@ -216,6 +217,15 @@ fn runtime_handle() -> tokio::runtime::Handle {
|
||||
})
|
||||
}
|
||||
|
||||
fn goto_is_empty(r: &Option<lsp::GotoDefinitionResponse>) -> bool {
|
||||
match r {
|
||||
None => true,
|
||||
Some(lsp::GotoDefinitionResponse::Scalar(_)) => false,
|
||||
Some(lsp::GotoDefinitionResponse::Array(v)) => v.is_empty(),
|
||||
Some(lsp::GotoDefinitionResponse::Link(v)) => v.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rpc_err(server: &str, method: &str, e: RpcError) -> IdeError {
|
||||
match e {
|
||||
RpcError::Cancelled => IdeError::cancelled(),
|
||||
@@ -1163,8 +1173,13 @@ impl LspManager {
|
||||
let mut out = Outcome::new();
|
||||
for t in targets.iter().filter(|t| snap.asks(t, pos)) {
|
||||
let r: IdeResult<Option<lsp::Hover>> =
|
||||
self.call(t, "textDocument/hover", snap.pos_params(t, pos), Some(&cancel), DEFAULT_TIMEOUT).await;
|
||||
loading::while_loading(&t.server, Some(&cancel), loading::LOADING_WAIT, Option::is_none, || {
|
||||
self.call(t, "textDocument/hover", snap.pos_params(t, pos), Some(&cancel), DEFAULT_TIMEOUT)
|
||||
})
|
||||
.await;
|
||||
match r {
|
||||
// Still loading: no documentation yet, not an error.
|
||||
Err(e) if e.kind == ErrorKind::TimedOut && t.server.is_loading() => out.value = Some(None),
|
||||
Ok(Some(h)) => {
|
||||
if let Some(h) = convert::hover(&self.conv(t.enc()), &snap.path, h) {
|
||||
return Ok(Some(h));
|
||||
@@ -1254,16 +1269,17 @@ impl LspManager {
|
||||
) -> IdeResult<Vec<Location>> {
|
||||
let mut out = Outcome::new();
|
||||
for t in targets.iter().filter(|t| snap.asks(t, pos)) {
|
||||
match self
|
||||
.call::<_, Option<lsp::GotoDefinitionResponse>>(
|
||||
let r = loading::while_loading(&t.server, cancel, loading::LOADING_WAIT, goto_is_empty, || {
|
||||
self.call::<_, Option<lsp::GotoDefinitionResponse>>(
|
||||
t,
|
||||
method,
|
||||
snap.pos_params(t, pos),
|
||||
cancel,
|
||||
DEFAULT_TIMEOUT,
|
||||
)
|
||||
.await
|
||||
{
|
||||
})
|
||||
.await;
|
||||
match r {
|
||||
Ok(Some(r)) => {
|
||||
let locs = convert::goto_response(&self.conv(t.enc()), r);
|
||||
let locs = library_sources(snap, pos, locs).await;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Requests answered while a server is still loading the project.
|
||||
//!
|
||||
//! rust-analyzer answers navigation with nothing until it has read the project's
|
||||
//! files, misses macro-generated items until build scripts and proc macros are
|
||||
//! built, and withdraws answers (`ContentModified`) when loading changes its
|
||||
//! data. Such an answer is not final: the request runs again as loading
|
||||
//! progresses, so ⌘B right after opening a project lands once the server can
|
||||
//! answer instead of reporting "not found". Only servers that report loading
|
||||
//! through `experimental/serverStatus` wait; for others nothing changes.
|
||||
|
||||
use crate::server::Server;
|
||||
use ide_core::error::ErrorKind;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
/// How long an explicit request waits for a server that is still loading.
|
||||
pub(crate) const LOADING_WAIT: Duration = Duration::from_secs(30);
|
||||
/// Retry interval while it loads, for phases that end without a notification.
|
||||
const POLL: Duration = Duration::from_secs(3);
|
||||
|
||||
/// Runs `query` on `server`. While the server is loading, an empty answer or a
|
||||
/// withdrawn one (`ContentModified`; not a cancellation of `cancel`) runs again
|
||||
/// whenever a loading phase ends, until it answers, loading ends, or `wait`
|
||||
/// passes; then a still-loading server yields a `TimedOut` error saying so.
|
||||
pub(crate) async fn while_loading<R, F, Fut>(
|
||||
server: &Server,
|
||||
cancel: Option<&CancellationToken>,
|
||||
wait: Duration,
|
||||
is_empty: impl Fn(&R) -> bool,
|
||||
query: F,
|
||||
) -> IdeResult<R>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = IdeResult<R>>,
|
||||
{
|
||||
let cancelled = || cancel.is_some_and(CancellationToken::is_cancelled);
|
||||
let deadline = tokio::time::Instant::now() + wait;
|
||||
loop {
|
||||
let phase = server.phase_notified();
|
||||
tokio::pin!(phase);
|
||||
phase.as_mut().enable();
|
||||
let asked_while_loading = server.is_loading();
|
||||
let result = query().await;
|
||||
let retry = match &result {
|
||||
Ok(r) => is_empty(r),
|
||||
Err(e) => e.kind == ErrorKind::Cancelled && !cancelled(),
|
||||
};
|
||||
if !retry || cancelled() || server.running().is_none() {
|
||||
return result;
|
||||
}
|
||||
if !server.is_loading() {
|
||||
// Loading ended while the server answered from its old state: ask once more.
|
||||
if asked_while_loading {
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
let now = tokio::time::Instant::now();
|
||||
if now >= deadline {
|
||||
return Err(IdeError::new(ErrorKind::TimedOut, format!("{} is still loading the project", server.name)));
|
||||
}
|
||||
let until = deadline.min(now + POLL);
|
||||
let stop = async {
|
||||
match cancel {
|
||||
Some(c) => c.cancelled().await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
() = phase => {}
|
||||
() = tokio::time::sleep_until(until) => {}
|
||||
() = stop => return Err(IdeError::cancelled()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,11 @@ pub struct Server {
|
||||
pub published_notify: Notify,
|
||||
/// Woken when the last `$/progress` task ends.
|
||||
idle_notify: Notify,
|
||||
/// `experimental/serverStatus` (rust-analyzer): `None` until the server sends
|
||||
/// one, then whether it is still loading the project.
|
||||
loading: Mutex<Option<bool>>,
|
||||
/// Woken whenever a `$/progress` task ends or the server's loading state changes.
|
||||
phase_notify: Notify,
|
||||
transient: Mutex<HashMap<PathBuf, Transient>>,
|
||||
pub java_build_lock: Arc<tokio::sync::Mutex<()>>,
|
||||
pub java_autobuild_suspended: std::sync::atomic::AtomicBool,
|
||||
@@ -144,6 +149,8 @@ impl Server {
|
||||
published: Mutex::new(HashMap::new()),
|
||||
published_notify: Notify::new(),
|
||||
idle_notify: Notify::new(),
|
||||
loading: Mutex::new(None),
|
||||
phase_notify: Notify::new(),
|
||||
transient: Mutex::new(HashMap::new()),
|
||||
java_build_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||
java_autobuild_suspended: std::sync::atomic::AtomicBool::new(false),
|
||||
@@ -389,22 +396,48 @@ impl Server {
|
||||
if self.running().is_none() {
|
||||
return;
|
||||
}
|
||||
let busy = !self.progress.lock().is_empty();
|
||||
let busy = self.is_busy();
|
||||
self.set_status(sink, if busy { ServerStatus::Busy } else { ServerStatus::Ready }, None);
|
||||
}
|
||||
|
||||
/// Whether the server reports work in progress (indexing, analysis).
|
||||
pub fn is_busy(&self) -> bool {
|
||||
!self.progress.lock().is_empty()
|
||||
!self.progress.lock().is_empty() || *self.loading.lock() == Some(true)
|
||||
}
|
||||
|
||||
/// Whether the server says it is still loading the project, so an empty
|
||||
/// answer may not be final. Only `serverStatus` tells: `$/progress` also
|
||||
/// covers background work (indexing, builds) during which answers are final.
|
||||
pub fn is_loading(&self) -> bool {
|
||||
*self.loading.lock() == Some(true)
|
||||
}
|
||||
|
||||
/// Records `experimental/serverStatus`: `quiescent` false while the project loads.
|
||||
pub fn set_loading(&self, sink: &SharedSink, loading: bool) {
|
||||
if self.loading.lock().replace(loading) == Some(loading) {
|
||||
return;
|
||||
}
|
||||
self.refresh_busy_status(sink);
|
||||
self.phase_ended();
|
||||
}
|
||||
|
||||
/// Called after a `$/progress` task ended.
|
||||
pub fn progress_ended(&self) {
|
||||
self.phase_ended();
|
||||
}
|
||||
|
||||
fn phase_ended(&self) {
|
||||
self.phase_notify.notify_waiters();
|
||||
if !self.is_busy() {
|
||||
self.idle_notify.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
/// Woken when the next loading phase ends.
|
||||
pub fn phase_notified(&self) -> tokio::sync::futures::Notified<'_> {
|
||||
self.phase_notify.notified()
|
||||
}
|
||||
|
||||
/// Waits until no `$/progress` task is active; false on timeout or cancellation.
|
||||
pub async fn wait_idle(&self, timeout: Duration, cancel: &CancellationToken) -> bool {
|
||||
let deadline = tokio::time::Instant::now() + timeout;
|
||||
@@ -499,6 +532,7 @@ impl Server {
|
||||
}
|
||||
|
||||
pub fn take_progress(&self) -> Vec<(TaskId, String)> {
|
||||
*self.loading.lock() = None;
|
||||
self.progress.lock().drain().map(|(_, p)| (p.task, p.title)).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -1009,6 +1009,78 @@ async fn progress_tasks_and_busy_status() {
|
||||
assert!(tasks[2].3);
|
||||
}
|
||||
|
||||
/// While a server says it is loading the project (rust-analyzer: `serverStatus`
|
||||
/// not quiescent) an empty or withdrawn answer is asked again once loading moves
|
||||
/// on. `$/progress` alone (background indexing, `cargo check`) doesn't wait.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn navigation_waits_for_a_server_that_is_still_loading() {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Answer {
|
||||
Empty,
|
||||
ContentModified,
|
||||
}
|
||||
for answer in [Answer::Empty, Answer::ContentModified] {
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
let (doc, path) = h.open("a.mk", "fn foo() {}\nfoo();\n");
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("mock");
|
||||
let peer = s.peer();
|
||||
peer.notify("experimental/serverStatus", json!({ "health": "ok", "quiescent": false })).unwrap();
|
||||
h.wait_status("mock", ServerStatus::Busy).await;
|
||||
if let Answer::ContentModified = answer {
|
||||
s.errors.lock().insert("textDocument/definition".into(), (-32801, "content modified".into()));
|
||||
}
|
||||
let mgr = h.mgr.clone();
|
||||
let goto = tokio::spawn(async move { mgr.goto(doc, Pos::new(1, 1), GotoKind::Definition).await });
|
||||
eventually(|| !s.requested("textDocument/definition").is_empty()).await;
|
||||
assert!(!goto.is_finished(), "{answer:?}: an answer while loading is not final");
|
||||
|
||||
s.errors.lock().clear();
|
||||
let r = json!({"start":{"line":0,"character":3},"end":{"line":0,"character":6}});
|
||||
s.respond("textDocument/definition", json!({ "uri": uri_of(&path), "range": r }));
|
||||
peer.notify("experimental/serverStatus", json!({ "health": "ok", "quiescent": true })).unwrap();
|
||||
let defs = tokio::time::timeout(Duration::from_secs(10), goto).await.unwrap().unwrap().unwrap();
|
||||
assert_eq!(defs, vec![ide_core::Location::file(path.clone(), rng(0, 3, 0, 6))], "{answer:?}");
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
}
|
||||
|
||||
// Hover documentation the same way.
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
let (doc, _) = h.open("a.mk", "x");
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("mock");
|
||||
s.peer().notify("experimental/serverStatus", json!({ "health": "ok", "quiescent": false })).unwrap();
|
||||
h.wait_status("mock", ServerStatus::Busy).await;
|
||||
let mgr = h.mgr.clone();
|
||||
let hover = tokio::spawn(async move { mgr.hover(doc, Pos::new(0, 0), Default::default()).await });
|
||||
eventually(|| !s.requested("textDocument/hover").is_empty()).await;
|
||||
s.respond("textDocument/hover", json!({ "contents": "docs" }));
|
||||
s.peer().notify("experimental/serverStatus", json!({ "health": "ok", "quiescent": true })).unwrap();
|
||||
let hover = tokio::time::timeout(Duration::from_secs(10), hover).await.unwrap().unwrap().unwrap();
|
||||
assert!(hover.is_some());
|
||||
|
||||
// "Nothing here" is the answer at once: rust-analyzer loaded with `cargo check`
|
||||
// running, and a server without `serverStatus` indexing in the background.
|
||||
for quiescent in [Some(true), None] {
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
let (doc, _) = h.open("a.mk", "x");
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("mock");
|
||||
let peer = s.peer();
|
||||
if let Some(q) = quiescent {
|
||||
peer.notify("experimental/serverStatus", json!({ "health": "ok", "quiescent": q })).unwrap();
|
||||
}
|
||||
peer.notify("$/progress", json!({ "token": "bg", "value": { "kind": "begin", "title": "Indexing" } })).unwrap();
|
||||
h.wait_status("mock", ServerStatus::Busy).await;
|
||||
let defs = tokio::time::timeout(Duration::from_secs(2), h.mgr.goto(doc, Pos::new(0, 0), GotoKind::Definition))
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("{quiescent:?}: no wait for background work"))
|
||||
.unwrap();
|
||||
assert!(defs.is_empty());
|
||||
assert_eq!(s.requested("textDocument/definition").len(), 1, "{quiescent:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn progress_titled_with_an_executable_path_shows_the_program_name() {
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
@@ -3333,7 +3405,8 @@ async fn interactive_capability_is_scoped_to_gopls() {
|
||||
let (doc, _) = h.open("probe.mk", "x\n");
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("mock");
|
||||
assert_eq!(s.requested("initialize")[0]["capabilities"]["experimental"], json!({}));
|
||||
let experimental = &s.requested("initialize")[0]["capabilities"]["experimental"];
|
||||
assert!(experimental.get("interactiveResolve").is_none(), "{experimental}");
|
||||
let form = h
|
||||
.mgr
|
||||
.resolve_command(
|
||||
|
||||
Reference in new issue
Block a user