Файли
openidea/crates/ide-lsp/src/handler.rs
T
l0ng-ai dadec04d62 fix(lsp): wait for a loading server before reporting no definition (#1444)
Right after a project opens, rust-analyzer answers navigation with nothing (or withdraws answers with ContentModified) until it has loaded the project, and the client treated both as final, so Go to Definition reported "Cannot find declaration to go to".

The client now enables experimental/serverStatus. While a server reports it is still loading (quiescent: false), an empty or withdrawn definition, declaration, type definition, implementation or hover answer is asked again each time a loading phase ends, for up to 30 s. Servers that don't send serverStatus behave as before.
2026-11-05 20:42:01 +08:00

369 рядки
16 KiB
Rust

//! Server → client notifications and requests.
use crate::LspManager;
use crate::convert;
use crate::server::{Progress, Server};
use crate::text::Encoding;
use crate::uri::uri_to_path;
use ide_core::event::{LspRefreshKind, NotifyLevel};
use ide_core::{IdeEvent, TaskId};
use ide_jsonrpc::{Handler, codes};
use lsp_types as lsp;
use serde_json::{Value, json};
use std::sync::{Arc, Weak};
/// Progress token standing for `angular/projectLoadingStart` … `Finish`.
const ANGULAR_LOADING: &str = "angular/projectLoading";
pub struct ClientHandler {
mgr: Weak<LspManager>,
server: Arc<Server>,
generation: u64,
}
impl ClientHandler {
pub fn new(mgr: Weak<LspManager>, server: Arc<Server>, generation: u64) -> Self {
Self { mgr, server, generation }
}
fn encoding(&self) -> Encoding {
self.server.running_gen(self.generation).map(|r| r.caps.read().encoding).unwrap_or_default()
}
/// Cancelling task `task` asks this server instance to stop the work of progress `token`.
fn cancel_handle(&self, task: TaskId, token: &Value) -> ide_core::task::CancelHandle {
let (server, generation, token) = (self.server.clone(), self.generation, token.clone());
ide_core::task::on_cancel(task, move || {
if let Some(run) = server.running_gen(generation)
&& let Err(e) = run.peer.notify("window/workDoneProgress/cancel", json!({ "token": token }))
{
tracing::debug!("{}: cannot cancel progress: {e}", server.name);
}
})
}
fn progress(&self, mgr: &LspManager, params: Value) {
let token = match &params["token"] {
Value::String(s) => s.clone(),
other => other.to_string(),
};
let value = &params["value"];
let message = value.get("message").and_then(Value::as_str).map(str::to_owned);
let fraction = value.get("percentage").and_then(Value::as_f64).map(|p| (p / 100.0).clamp(0.0, 1.0) as f32);
let cancellable = value.get("cancellable").and_then(Value::as_bool);
let event = {
let mut progress = self.server.progress.lock();
match value.get("kind").and_then(Value::as_str) {
Some("begin") => {
let title = value
.get("title")
.and_then(Value::as_str)
.filter(|t| !t.is_empty())
.map(ide_core::task::display_title)
.unwrap_or_else(|| self.server.name.clone());
let task = TaskId::next();
let cancel = (cancellable == Some(true)).then(|| self.cancel_handle(task, &params["token"]));
let cancellable = cancel.is_some();
progress.insert(token, Progress { task, title: title.clone(), message: message.clone(), cancel });
IdeEvent::Task { id: task, title, message, fraction, done: false, cancellable }
}
Some("report") => {
let Some(p) = progress.get_mut(&token) else { return };
if message.is_some() {
p.message = message;
}
match cancellable {
Some(true) if p.cancel.is_none() => {
p.cancel = Some(self.cancel_handle(p.task, &params["token"]))
}
Some(false) => p.cancel = None,
_ => {}
}
IdeEvent::Task {
id: p.task,
title: p.title.clone(),
message: p.message.clone(),
fraction,
done: false,
cancellable: p.cancel.is_some(),
}
}
Some("end") => {
let Some(p) = progress.remove(&token) else { return };
IdeEvent::Task {
id: p.task,
title: p.title,
message: message.or(p.message),
fraction: Some(1.0),
done: true,
cancellable: false,
}
}
_ => return,
}
};
let ended = matches!(&event, IdeEvent::Task { done: true, .. });
mgr.sink().emit(event);
self.server.refresh_busy_status(mgr.sink());
if ended {
self.server.progress_ended();
}
}
fn publish_diagnostics(&self, mgr: &LspManager, params: Value) {
// Problems come only from servers used for `diagnostics`.
if self.server.running_gen(self.generation).is_some_and(|run| !run.caps.read().restriction.diagnostics()) {
return;
}
let params: lsp::PublishDiagnosticsParams = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => {
tracing::debug!("{}: bad publishDiagnostics: {e}", self.server.name);
return;
}
};
let Some(path) = uri_to_path(params.uri.as_str()) else { return };
// Files opened only for a request (references search) are not the user's problems.
if (self.server.external_diagnostics.load(std::sync::atomic::Ordering::Acquire)
&& !mgr.docs.lock().values().any(|d| d.path == path))
|| self.server.suppresses_diagnostics(&path)
{
return;
}
let shown = self.server.set_diagnostics(path.clone(), params.diagnostics);
mgr.emit_diagnostics(&self.server, path, shown, self.encoding());
}
fn show_message(&self, mgr: &LspManager, params: &Value) {
let text = params.get("message").and_then(Value::as_str).unwrap_or_default().to_owned();
let level = match params.get("type").and_then(Value::as_u64) {
Some(1) => NotifyLevel::Error,
Some(2) => NotifyLevel::Warning,
_ => {
tracing::info!("[{}] {text}", self.server.name);
return;
}
};
if !mgr.first_message(&self.server.name, &text) {
tracing::debug!("[{}] repeated: {text}", self.server.name);
return;
}
mgr.sink().emit(IdeEvent::Notify { level, title: self.server.name.clone(), body: Some(text) });
}
fn configuration(&self, params: &Value) -> Value {
let options = all_settings(&self.server.effective_config());
let items = params.get("items").and_then(Value::as_array).cloned().unwrap_or_default();
Value::Array(
items
.iter()
// vtsls asks for the whole configuration as section `""`.
.map(|item| match item.get("section").and_then(Value::as_str).filter(|s| !s.is_empty()) {
// The server's own section: its block under `settings` (basedpyright asks
// `basedpyright` and `basedpyright.analysis`), else the options themselves.
Some(section) if section == self.server.name => {
options.get("settings").and_then(|s| s.get(section)).unwrap_or(&options).clone()
}
// Servers that take settings under `initializationOptions.settings`
// (jdtls) ask for the same sections later.
Some(section) => [&options, options.get("settings").unwrap_or(&Value::Null)]
.into_iter()
.find_map(|base| section.split('.').try_fold(base, |v, key| v.get(key)))
.cloned()
.unwrap_or(Value::Null),
None => options.clone(),
})
.collect(),
)
}
fn apply_edit(&self, mgr: &LspManager, params: Value) -> Result<Value, (i64, String)> {
let edit: lsp::WorkspaceEdit = params
.get("edit")
.cloned()
.ok_or_else(|| (codes::INVALID_PARAMS, "missing edit".to_owned()))
.and_then(|e| serde_json::from_value(e).map_err(|e| (codes::INVALID_PARAMS, e.to_string())))?;
// Convert before calling the hook: it re-enters document sync.
let edit = convert::workspace_edit(&mgr.conv(self.encoding()), edit);
let applied = match mgr.apply_edit_hook() {
Some(hook) => hook(edit.clone()),
None => false,
};
if applied {
let _ = mgr.applied_edits.send((self.server.id, edit));
}
let mut reply = json!({ "applied": applied });
if !applied {
reply["failureReason"] = json!("edit could not be applied");
}
Ok(reply)
}
fn registration(&self, mgr: &LspManager, params: &Value, register: bool) {
let Some(run) = self.server.initialized_gen(self.generation) else { return };
let key = if register { "registrations" } else { "unregisterations" };
let list = params.get(key).or_else(|| params.get("unregistrations")).and_then(Value::as_array);
let mut caps = run.caps.write();
let mut watchers_changed = false;
for reg in list.into_iter().flatten() {
let Some(method) = reg.get("method").and_then(Value::as_str) else { continue };
if method == "workspace/didChangeWatchedFiles" {
let id = reg.get("id").and_then(Value::as_str).unwrap_or_default();
let mut watchers = run.watchers.write();
if register {
watchers.register(id, reg.get("registerOptions"));
} else {
watchers.unregister(id);
}
watchers_changed = true;
} else if register {
caps.register(method, reg.get("registerOptions"));
} else {
caps.unregister(method);
}
}
drop(caps);
if watchers_changed {
mgr.file_watch_roots_changed();
}
}
}
impl Handler for ClientHandler {
fn on_notification(&self, method: &str, params: Value) {
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, &params),
"window/logMessage" => {
let text = params.get("message").and_then(Value::as_str).unwrap_or_default();
match params.get("type").and_then(Value::as_u64) {
Some(1) if self.server.name == "jdtls" && text.contains(crate::jdtls::reserved_names::IMPORT_FAILURE) => {
tracing::warn!("[{}] {text}", self.server.name);
mgr.report_import_failure(self.server.root.clone(), text.to_owned());
}
Some(1) => tracing::warn!("[{}] {text}", self.server.name),
Some(2 | 3) => tracing::info!("[{}] {text}", self.server.name),
_ => tracing::debug!("[{}] {text}", self.server.name),
}
}
"tsserver/request" => mgr.forward_tsserver_requests(self.server.clone(), self.generation, params),
"language/status" if self.server.name == "jdtls" && params["type"] == "ServiceReady" => {
mgr.java_import_ready(self.server.clone(), self.generation);
}
"language/status" if self.server.name == "jdtls" && params["type"] == "ProjectStatus" => {
mgr.java_projects_updated(self.server.clone(), self.generation);
}
// The Angular server's project loading, shown like `$/progress`; one
// task while several projects load.
"angular/projectLoadingStart" if !self.server.progress.lock().contains_key(ANGULAR_LOADING) => self
.progress(
&mgr,
json!({ "token": ANGULAR_LOADING, "value": { "kind": "begin", "title": "Loading Angular project" } }),
),
"angular/projectLoadingStart" => {}
"angular/projectLoadingFinish" => {
self.progress(&mgr, json!({ "token": ANGULAR_LOADING, "value": { "kind": "end" } }))
}
"telemetry/event" | "$/logTrace" | "$/cancelRequest" => {}
other => tracing::trace!("[{}] unhandled notification {other}", self.server.name),
}
}
fn on_request(&self, method: &str, params: Value) -> Result<Value, (i64, String)> {
let Some(mgr) = self.mgr.upgrade() else {
return Err((codes::INTERNAL_ERROR, "client is shutting down".into()));
};
match method {
"window/workDoneProgress/create" => Ok(Value::Null),
"window/showMessageRequest" => {
self.show_message(&mgr, &params);
Ok(Value::Null)
}
"window/showDocument" => Ok(json!({ "success": false })),
"workspace/configuration" => Ok(self.configuration(&params)),
"workspace/workspaceFolders" => Ok(mgr.workspace_folders(&self.server)),
"client/registerCapability" => {
self.registration(&mgr, &params, true);
Ok(Value::Null)
}
"client/unregisterCapability" => {
self.registration(&mgr, &params, false);
Ok(Value::Null)
}
"workspace/applyEdit" => self.apply_edit(&mgr, params),
"workspace/semanticTokens/refresh" | "workspace/inlayHint/refresh" | "workspace/codeLens/refresh" => {
let kind = match method {
"workspace/semanticTokens/refresh" => LspRefreshKind::SemanticTokens,
"workspace/inlayHint/refresh" => LspRefreshKind::InlayHints,
_ => LspRefreshKind::CodeLens,
};
mgr.sink().emit(IdeEvent::LspRefresh { server: self.server.id, kind });
Ok(Value::Null)
}
"workspace/diagnostic/refresh" => {
mgr.refresh_pulled_diagnostics(&self.server);
Ok(Value::Null)
}
"workspace/inlineValue/refresh" => Ok(Value::Null),
other => Err((codes::METHOD_NOT_FOUND, format!("unsupported method {other}"))),
}
}
fn after_response(&self, method: &str) {
if method == "workspace/configuration"
&& self.server.config.resync_on_configuration
&& let Some(mgr) = self.mgr.upgrade()
{
mgr.resync_documents(&self.server, self.generation);
}
}
}
/// What `workspace/configuration` sections are looked up in: the server's
/// `initializationOptions` with its `settings` merged over them.
pub(crate) fn all_settings(cfg: &ide_language::LanguageServerConfig) -> Value {
let mut out = cfg.initialization_options.as_ref().and_then(|v| serde_json::to_value(v).ok()).unwrap_or(Value::Null);
if cfg.name == "jdtls" {
out = out.get("settings").cloned().unwrap_or_else(|| json!({}));
if let Some(settings) = &cfg.settings {
// Accept the initialization-options wrapper as well as LSP settings.
if let Some(nested) = settings.get("settings") {
merge(&mut out, nested.clone());
}
let mut direct = settings.clone();
if let Some(direct) = direct.as_object_mut() {
direct.remove("settings");
}
merge(&mut out, direct);
}
return out;
}
if let Some(settings) = &cfg.settings {
merge(&mut out, settings.clone());
}
out
}
fn merge(base: &mut Value, layer: Value) {
match (base, layer) {
(Value::Object(base), Value::Object(layer)) => {
for (k, v) in layer {
match base.get_mut(&k) {
Some(existing) => merge(existing, v),
None => {
base.insert(k, v);
}
}
}
}
(base, layer) => *base = layer,
}
}