дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
237 рядки
9.9 KiB
Rust
237 рядки
9.9 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};
|
|
|
|
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()
|
|
}
|
|
|
|
fn progress(&self, mgr: &LspManager, params: Value) {
|
|
let token = match ¶ms["token"] {
|
|
Value::String(s) => s.clone(),
|
|
other => other.to_string(),
|
|
};
|
|
let value = ¶ms["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 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(str::to_owned)
|
|
.unwrap_or_else(|| self.server.name.clone());
|
|
let task = TaskId::next();
|
|
progress.insert(token, Progress { task, title: title.clone(), message: message.clone() });
|
|
IdeEvent::Task { id: task, title, message, fraction, done: false }
|
|
}
|
|
Some("report") => {
|
|
let Some(p) = progress.get_mut(&token) else { return };
|
|
if message.is_some() {
|
|
p.message = message;
|
|
}
|
|
IdeEvent::Task {
|
|
id: p.task,
|
|
title: p.title.clone(),
|
|
message: p.message.clone(),
|
|
fraction,
|
|
done: false,
|
|
}
|
|
}
|
|
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,
|
|
}
|
|
}
|
|
_ => return,
|
|
}
|
|
};
|
|
mgr.sink().emit(event);
|
|
self.server.refresh_busy_status(mgr.sink());
|
|
}
|
|
|
|
fn publish_diagnostics(&self, mgr: &LspManager, params: Value) {
|
|
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 };
|
|
let conv = mgr.conv(self.encoding());
|
|
let items = params
|
|
.diagnostics
|
|
.iter()
|
|
.cloned()
|
|
.map(|d| convert::diagnostic(&conv, &path, d, &self.server.name))
|
|
.collect();
|
|
self.server.set_diagnostics(path.clone(), params.diagnostics);
|
|
mgr.sink().emit(IdeEvent::Diagnostics { path: path.into(), source: self.server.name.clone(), items });
|
|
}
|
|
|
|
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 = self
|
|
.server
|
|
.effective_config()
|
|
.initialization_options
|
|
.as_ref()
|
|
.and_then(|v| serde_json::to_value(v).ok())
|
|
.unwrap_or(Value::Null);
|
|
let items = params.get("items").and_then(Value::as_array).cloned().unwrap_or_default();
|
|
Value::Array(
|
|
items
|
|
.iter()
|
|
.map(|item| match item.get("section").and_then(Value::as_str) {
|
|
Some(section) if section == self.server.name => 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),
|
|
None => false,
|
|
};
|
|
let mut reply = json!({ "applied": applied });
|
|
if !applied {
|
|
reply["failureReason"] = json!("edit could not be applied");
|
|
}
|
|
Ok(reply)
|
|
}
|
|
|
|
fn registration(&self, params: &Value, register: bool) {
|
|
let Some(run) = self.server.running_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();
|
|
for reg in list.into_iter().flatten() {
|
|
let Some(method) = reg.get("method").and_then(Value::as_str) else { continue };
|
|
if register {
|
|
caps.register(method, reg.get("registerOptions"));
|
|
} else {
|
|
caps.unregister(method);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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),
|
|
"textDocument/publishDiagnostics" => self.publish_diagnostics(&mgr, params),
|
|
"window/showMessage" => self.show_message(&mgr, ¶ms),
|
|
"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) => tracing::warn!("[{}] {text}", self.server.name),
|
|
Some(2 | 3) => tracing::info!("[{}] {text}", self.server.name),
|
|
_ => tracing::debug!("[{}] {text}", self.server.name),
|
|
}
|
|
}
|
|
"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, ¶ms);
|
|
Ok(Value::Null)
|
|
}
|
|
"window/showDocument" => Ok(json!({ "success": false })),
|
|
"workspace/configuration" => Ok(self.configuration(¶ms)),
|
|
"workspace/workspaceFolders" => Ok(mgr.workspace_folders(&self.server)),
|
|
"client/registerCapability" => {
|
|
self.registration(¶ms, true);
|
|
Ok(Value::Null)
|
|
}
|
|
"client/unregisterCapability" => {
|
|
self.registration(¶ms, 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" | "workspace/inlineValue/refresh" => Ok(Value::Null),
|
|
other => Err((codes::METHOD_NOT_FOUND, format!("unsupported method {other}"))),
|
|
}
|
|
}
|
|
}
|