use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use tokio::sync::Notify;
use tracing::{debug, trace, warn};
use super::client::DiagnosticsCache;
use super::extract;
use super::protocol::RpcError;
use super::state::{ProgressTracker, ServerLifecycle};
#[allow(
clippy::struct_excessive_bools,
reason = "independent capability flags from LSP init"
)]
pub struct LspServer {
capabilities: OnceLock<Value>,
supports_pull_diagnostics: OnceLock<bool>,
supports_hover: OnceLock<bool>,
supports_definition: OnceLock<bool>,
supports_references: OnceLock<bool>,
supports_document_symbols: OnceLock<bool>,
supports_workspace_symbols: OnceLock<bool>,
supports_workspace_symbol_resolve: OnceLock<bool>,
supports_rename: OnceLock<bool>,
supports_type_definition: OnceLock<bool>,
supports_implementation: OnceLock<bool>,
supports_call_hierarchy: OnceLock<bool>,
supports_type_hierarchy: OnceLock<bool>,
supports_code_action: OnceLock<bool>,
pub(crate) diagnostics: DiagnosticsCache,
pub(crate) diagnostics_generation: Arc<Mutex<HashMap<String, u64>>>,
pub(crate) diagnostics_notify: Arc<Notify>,
pub(crate) capability_notify: Arc<Notify>,
pub(crate) progress: Arc<Mutex<ProgressTracker>>,
pub(crate) progress_notify: Arc<Notify>,
pub(crate) lifecycle: Arc<Mutex<ServerLifecycle>>,
pub(crate) state_notify: Arc<Notify>,
pub(crate) ever_busy: AtomicBool,
pub(crate) publishes_version: Arc<AtomicBool>,
pub(crate) language: String,
settings: Option<Value>,
}
impl LspServer {
#[must_use]
pub fn new(language: String, settings: Option<Value>) -> Self {
Self {
capabilities: OnceLock::new(),
supports_pull_diagnostics: OnceLock::new(),
supports_hover: OnceLock::new(),
supports_definition: OnceLock::new(),
supports_references: OnceLock::new(),
supports_document_symbols: OnceLock::new(),
supports_workspace_symbols: OnceLock::new(),
supports_workspace_symbol_resolve: OnceLock::new(),
supports_rename: OnceLock::new(),
supports_type_definition: OnceLock::new(),
supports_implementation: OnceLock::new(),
supports_call_hierarchy: OnceLock::new(),
supports_type_hierarchy: OnceLock::new(),
supports_code_action: OnceLock::new(),
diagnostics: Arc::new(Mutex::new(HashMap::new())),
diagnostics_generation: Arc::new(Mutex::new(HashMap::new())),
diagnostics_notify: Arc::new(Notify::new()),
capability_notify: Arc::new(Notify::new()),
progress: Arc::new(Mutex::new(ProgressTracker::new())),
progress_notify: Arc::new(Notify::new()),
lifecycle: Arc::new(Mutex::new(ServerLifecycle::Initializing)),
state_notify: Arc::new(Notify::new()),
ever_busy: AtomicBool::new(false),
publishes_version: Arc::new(AtomicBool::new(false)),
language,
settings,
}
}
pub(crate) const fn settings(&self) -> Option<&Value> {
self.settings.as_ref()
}
pub fn set_capabilities(&self, capabilities: Value) {
let has = |key: &str| {
capabilities
.get(key)
.is_some_and(|v| v.as_bool() != Some(false) && !v.is_null())
};
let _ = self
.supports_pull_diagnostics
.set(has("diagnosticProvider"));
let _ = self.supports_hover.set(has("hoverProvider"));
let _ = self.supports_definition.set(has("definitionProvider"));
let _ = self.supports_references.set(has("referencesProvider"));
let _ = self
.supports_document_symbols
.set(has("documentSymbolProvider"));
let _ = self
.supports_workspace_symbols
.set(has("workspaceSymbolProvider"));
let _ = self.supports_workspace_symbol_resolve.set(
capabilities
.get("workspaceSymbolProvider")
.and_then(|v| v.get("resolveProvider"))
.and_then(Value::as_bool)
.unwrap_or(false),
);
let _ = self.supports_rename.set(has("renameProvider"));
let _ = self
.supports_type_definition
.set(has("typeDefinitionProvider"));
let _ = self
.supports_implementation
.set(has("implementationProvider"));
let _ = self
.supports_call_hierarchy
.set(has("callHierarchyProvider"));
let _ = self
.supports_type_hierarchy
.set(has("typeHierarchyProvider"));
let _ = self.supports_code_action.set(has("codeActionProvider"));
let _ = self.capabilities.set(capabilities);
}
pub fn capabilities(&self) -> &Value {
static EMPTY: OnceLock<Value> = OnceLock::new();
self.capabilities
.get()
.unwrap_or_else(|| EMPTY.get_or_init(|| Value::Object(serde_json::Map::new())))
}
pub fn supports_pull_diagnostics(&self) -> bool {
self.supports_pull_diagnostics
.get()
.copied()
.unwrap_or(false)
}
pub fn supports_hover(&self) -> bool {
self.supports_hover.get().copied().unwrap_or(false)
}
pub fn supports_definition(&self) -> bool {
self.supports_definition.get().copied().unwrap_or(false)
}
pub fn supports_references(&self) -> bool {
self.supports_references.get().copied().unwrap_or(false)
}
pub fn supports_document_symbols(&self) -> bool {
self.supports_document_symbols
.get()
.copied()
.unwrap_or(false)
}
pub fn supports_workspace_symbols(&self) -> bool {
self.supports_workspace_symbols
.get()
.copied()
.unwrap_or(false)
}
pub fn supports_workspace_symbol_resolve(&self) -> bool {
self.supports_workspace_symbol_resolve
.get()
.copied()
.unwrap_or(false)
}
pub fn supports_rename(&self) -> bool {
self.supports_rename.get().copied().unwrap_or(false)
}
pub fn supports_type_definition(&self) -> bool {
self.supports_type_definition
.get()
.copied()
.unwrap_or(false)
}
pub fn supports_implementation(&self) -> bool {
self.supports_implementation.get().copied().unwrap_or(false)
}
pub fn supports_call_hierarchy(&self) -> bool {
self.supports_call_hierarchy.get().copied().unwrap_or(false)
}
pub fn supports_type_hierarchy(&self) -> bool {
self.supports_type_hierarchy.get().copied().unwrap_or(false)
}
pub fn supports_code_action(&self) -> bool {
self.supports_code_action.get().copied().unwrap_or(false)
}
pub fn sends_progress(&self) -> bool {
self.ever_busy.load(Ordering::SeqCst)
}
pub fn in_progress_count(&self) -> u32 {
match self.lifecycle() {
ServerLifecycle::Busy(n) => n,
_ => 0,
}
}
pub fn lifecycle(&self) -> ServerLifecycle {
self.lifecycle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn set_lifecycle(&self, state: ServerLifecycle) {
let mut lifecycle = self
.lifecycle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*lifecycle = state;
drop(lifecycle);
self.state_notify.notify_waiters();
}
#[allow(clippy::too_many_lines, reason = "match dispatcher with per-arm logic")]
pub fn on_notification(&self, method: &str, params: &Value) {
match method {
"textDocument/publishDiagnostics" => {
let Some(uri) = extract::publish_diagnostics_uri(params) else {
warn!("publishDiagnostics missing uri");
return;
};
let version = extract::publish_diagnostics_version(params);
let diagnostics = extract::publish_diagnostics_diagnostics(params);
debug!(
"Received {} diagnostics for {:?} (version={:?})",
diagnostics.len(),
uri,
version,
);
if version.is_some() && !self.publishes_version.swap(true, Ordering::SeqCst) {
self.capability_notify.notify_waiters();
}
let mut cache = self
.diagnostics
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
cache.insert(uri.to_string(), (version, diagnostics));
drop(cache);
let mut generations = self
.diagnostics_generation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let counter = generations.entry(uri.to_string()).or_insert(0);
*counter += 1;
drop(generations);
self.diagnostics_notify.notify_waiters();
}
"$/progress" => {
let Some(token_value) = extract::progress_token(params) else {
warn!("$/progress missing token");
return;
};
let token_str = token_value
.as_str()
.map_or_else(|| token_value.to_string(), str::to_string);
let mut tracker = self
.progress
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
tracker.update(&token_str, ¶ms["value"]);
if tracker.broadcast_changed()
&& let Some(p) = tracker.primary_progress()
{
debug!("Progress: {} {}%", p.title, p.percentage.unwrap_or(0));
}
drop(tracker);
let kind = params["value"]["kind"].as_str();
let mut lifecycle = self
.lifecycle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if lifecycle.is_terminal() {
return;
}
match kind {
Some("begin") => {
let first = !self.ever_busy.swap(true, Ordering::SeqCst);
*lifecycle = match *lifecycle {
ServerLifecycle::Busy(n) => ServerLifecycle::Busy(n + 1),
_ => ServerLifecycle::Busy(1),
};
drop(lifecycle);
if first {
self.capability_notify.notify_waiters();
}
}
Some("end") => {
*lifecycle = match *lifecycle {
ServerLifecycle::Busy(n) if n > 1 => ServerLifecycle::Busy(n - 1),
ServerLifecycle::Busy(1) => {
debug!("Server ready (progress completed)");
ServerLifecycle::Healthy
}
ref other => other.clone(),
};
drop(lifecycle);
}
_ => {
drop(lifecycle);
}
}
self.progress_notify.notify_waiters();
self.state_notify.notify_waiters();
}
"window/logMessage" | "window/showMessage" => {
if let Some(message) = params.get("message").and_then(|m| m.as_str()) {
debug!("LSP server message: {}", message);
}
}
_ => {
trace!("Ignoring notification: {} params={}", method, params);
}
}
}
pub fn on_request(&self, method: &str, params: &Value) -> Result<Value, RpcError> {
match method {
"workspace/configuration" => {
let items = params.get("items").and_then(Value::as_array);
let item_count = items.map_or(1, Vec::len);
let results: Vec<Value> = (0..item_count)
.map(|i| {
let section = items
.and_then(|arr| arr.get(i))
.and_then(|item| item.get("section"))
.and_then(Value::as_str);
resolve_section(self.settings.as_ref(), section)
})
.collect();
Ok(Value::Array(results))
}
"window/workDoneProgress/create"
| "client/registerCapability"
| "client/unregisterCapability"
| "window/showMessageRequest" => Ok(Value::Null),
_ => Err(RpcError {
code: -32601,
message: format!("Method '{method}' not supported by client"),
}),
}
}
pub fn on_shutdown(&self) {
self.set_lifecycle(ServerLifecycle::Dead);
if let Ok(mut progress) = self.progress.lock() {
progress.clear();
}
self.diagnostics_notify.notify_waiters();
}
pub fn is_progress_active(&self) -> bool {
self.progress
.try_lock()
.map_or(true, |tracker| tracker.is_busy())
}
pub fn state_notify(&self) -> &Notify {
&self.state_notify
}
}
fn resolve_section(settings: Option<&Value>, section: Option<&str>) -> Value {
let (Some(mut current), Some(section)) = (settings, section) else {
return Value::Object(serde_json::Map::new());
};
for key in section.split('.') {
match current.get(key) {
Some(child) => current = child,
None => return Value::Object(serde_json::Map::new()),
}
}
current.clone()
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests use expect for readable assertions"
)]
mod tests {
use super::*;
use serde_json::json;
fn test_server() -> LspServer {
LspServer::new("test".to_string(), None)
}
fn server_with_caps(caps: Value) -> LspServer {
let server = test_server();
server.set_capabilities(caps);
server
}
#[test]
fn set_capabilities_extracts_pull_diagnostics() {
let server =
server_with_caps(json!({ "diagnosticProvider": { "interFileDependencies": true } }));
assert!(server.supports_pull_diagnostics());
}
#[test]
fn no_diagnostic_provider() {
let server = server_with_caps(json!({}));
assert!(!server.supports_pull_diagnostics());
}
#[test]
fn before_set_capabilities_nothing_supported() {
let server = test_server();
assert!(!server.supports_pull_diagnostics());
assert!(!server.supports_hover());
assert!(!server.supports_workspace_symbols());
assert_eq!(server.capabilities(), &json!({}));
}
#[test]
fn lifecycle_starts_initializing() {
let server = test_server();
assert_eq!(server.lifecycle(), ServerLifecycle::Initializing);
assert!(!server.sends_progress());
}
#[test]
fn set_lifecycle_transitions_and_notifies() {
let server = test_server();
server.set_lifecycle(ServerLifecycle::Healthy);
assert_eq!(server.lifecycle(), ServerLifecycle::Healthy);
server.set_lifecycle(ServerLifecycle::Dead);
assert_eq!(server.lifecycle(), ServerLifecycle::Dead);
}
#[test]
fn supports_capability_true() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": true }));
assert!(server.supports_workspace_symbols());
}
#[test]
fn supports_capability_false() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": false }));
assert!(!server.supports_workspace_symbols());
}
#[test]
fn supports_capability_options_object() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": {} }));
assert!(server.supports_workspace_symbols());
}
#[test]
fn supports_capability_detailed_options() {
let server = server_with_caps(json!({
"workspaceSymbolProvider": { "resolveProvider": true }
}));
assert!(server.supports_workspace_symbols());
}
#[test]
fn supports_capability_missing() {
let server = server_with_caps(json!({}));
assert!(!server.supports_workspace_symbols());
}
#[test]
fn supports_capability_null() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": null }));
assert!(!server.supports_workspace_symbols());
}
#[test]
fn explicit_false_not_supported() {
let server = server_with_caps(json!({
"hoverProvider": false,
"definitionProvider": false,
"referencesProvider": false,
"documentSymbolProvider": false,
"workspaceSymbolProvider": false,
"renameProvider": false,
"typeDefinitionProvider": false,
"implementationProvider": false,
"callHierarchyProvider": false,
"typeHierarchyProvider": false,
"codeActionProvider": false,
}));
assert!(!server.supports_hover());
assert!(!server.supports_definition());
assert!(!server.supports_references());
assert!(!server.supports_document_symbols());
assert!(!server.supports_workspace_symbols());
assert!(!server.supports_rename());
assert!(!server.supports_type_definition());
assert!(!server.supports_implementation());
assert!(!server.supports_call_hierarchy());
assert!(!server.supports_type_hierarchy());
assert!(!server.supports_code_action());
}
#[test]
fn empty_capabilities_nothing_supported() {
let server = server_with_caps(json!({}));
assert!(!server.supports_hover());
assert!(!server.supports_definition());
assert!(!server.supports_references());
assert!(!server.supports_document_symbols());
assert!(!server.supports_workspace_symbols());
assert!(!server.supports_workspace_symbol_resolve());
assert!(!server.supports_rename());
assert!(!server.supports_type_definition());
assert!(!server.supports_implementation());
assert!(!server.supports_call_hierarchy());
assert!(!server.supports_type_hierarchy());
assert!(!server.supports_code_action());
}
#[test]
fn supports_all_capabilities() {
let server = server_with_caps(json!({
"hoverProvider": true,
"definitionProvider": true,
"referencesProvider": true,
"documentSymbolProvider": true,
"workspaceSymbolProvider": { "resolveProvider": true },
"renameProvider": true,
"typeDefinitionProvider": true,
"implementationProvider": true,
"callHierarchyProvider": true,
"typeHierarchyProvider": true,
"codeActionProvider": true,
}));
assert!(server.supports_hover());
assert!(server.supports_definition());
assert!(server.supports_references());
assert!(server.supports_document_symbols());
assert!(server.supports_workspace_symbols());
assert!(server.supports_workspace_symbol_resolve());
assert!(server.supports_rename());
assert!(server.supports_type_definition());
assert!(server.supports_implementation());
assert!(server.supports_call_hierarchy());
assert!(server.supports_type_hierarchy());
assert!(server.supports_code_action());
}
#[test]
fn workspace_symbol_resolve_boolean_provider() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": true }));
assert!(server.supports_workspace_symbols());
assert!(!server.supports_workspace_symbol_resolve());
}
#[test]
fn workspace_symbol_resolve_empty_options() {
let server = server_with_caps(json!({ "workspaceSymbolProvider": {} }));
assert!(server.supports_workspace_symbols());
assert!(!server.supports_workspace_symbol_resolve());
}
#[test]
fn workspace_symbol_resolve_false() {
let server = server_with_caps(json!({
"workspaceSymbolProvider": { "resolveProvider": false }
}));
assert!(server.supports_workspace_symbols());
assert!(!server.supports_workspace_symbol_resolve());
}
#[test]
fn workspace_symbol_resolve_true() {
let server = server_with_caps(json!({
"workspaceSymbolProvider": { "resolveProvider": true }
}));
assert!(server.supports_workspace_symbols());
assert!(server.supports_workspace_symbol_resolve());
}
#[test]
fn resolve_section_traverses_dot_path() {
let settings = json!({
"python": {
"analysis": {
"exclude": ["**/target"],
"extraPaths": []
},
"pythonPath": "/usr/bin/python3"
}
});
assert_eq!(
resolve_section(Some(&settings), Some("python.analysis")),
json!({"exclude": ["**/target"], "extraPaths": []})
);
assert_eq!(
resolve_section(Some(&settings), Some("python.pythonPath")),
json!("/usr/bin/python3")
);
assert_eq!(
resolve_section(Some(&settings), Some("python")),
json!({"analysis": {"exclude": ["**/target"], "extraPaths": []}, "pythonPath": "/usr/bin/python3"})
);
}
#[test]
fn resolve_section_missing_path_returns_empty_object() {
let settings = json!({"python": {"analysis": {}}});
assert_eq!(resolve_section(Some(&settings), Some("rust")), json!({}));
assert_eq!(
resolve_section(Some(&settings), Some("python.nonexistent")),
json!({})
);
}
#[test]
fn resolve_section_none_settings_returns_empty_object() {
assert_eq!(resolve_section(None, Some("python")), json!({}));
}
#[test]
fn resolve_section_none_section_returns_empty_object() {
let settings = json!({"python": {}});
assert_eq!(resolve_section(Some(&settings), None), json!({}));
}
#[test]
fn configuration_request_uses_settings() {
let server = LspServer::new(
"test".to_string(),
Some(json!({"mockls": {"key": "value"}})),
);
let result = server
.on_request(
"workspace/configuration",
&json!({"items": [{"section": "mockls"}]}),
)
.expect("configuration request should succeed");
assert_eq!(result, json!([{"key": "value"}]));
}
#[test]
fn configuration_request_without_settings_returns_empty_objects() {
let server = test_server();
let result = server
.on_request(
"workspace/configuration",
&json!({"items": [{"section": "mockls"}, {"section": "other"}]}),
)
.expect("configuration request should succeed");
assert_eq!(result, json!([{}, {}]));
}
#[test]
fn register_capability_accepted() {
let server = test_server();
let result = server
.on_request(
"client/registerCapability",
&json!({"registrations": [{"id": "1", "method": "textDocument/didChangeConfiguration"}]}),
)
.expect("registerCapability should succeed");
assert_eq!(result, Value::Null);
}
#[test]
fn unregister_capability_accepted() {
let server = test_server();
let result = server
.on_request(
"client/unregisterCapability",
&json!({"unregisterations": [{"id": "1", "method": "textDocument/didChangeConfiguration"}]}),
)
.expect("unregisterCapability should succeed");
assert_eq!(result, Value::Null);
}
#[test]
fn show_message_request_accepted() {
let server = test_server();
let result = server
.on_request(
"window/showMessageRequest",
&json!({"type": 1, "message": "Restart?", "actions": [{"title": "Yes"}]}),
)
.expect("showMessageRequest should succeed");
assert_eq!(result, Value::Null);
}
#[test]
fn unknown_request_rejected() {
let server = test_server();
let err = server
.on_request("custom/unknownMethod", &json!({}))
.expect_err("unknown method should be rejected");
assert_eq!(err.code, -32601);
}
#[test]
fn is_progress_active_begin_end() {
let server = test_server();
assert!(!server.is_progress_active());
server.on_notification(
"$/progress",
&json!({
"token": "test-token",
"value": { "kind": "begin", "title": "Indexing", "percentage": 0 }
}),
);
assert!(server.is_progress_active());
server.on_notification(
"$/progress",
&json!({
"token": "test-token",
"value": { "kind": "end" }
}),
);
assert!(!server.is_progress_active());
}
#[test]
fn publish_diagnostics_updates_cache_and_generation() {
let server = test_server();
server.on_notification(
"textDocument/publishDiagnostics",
&json!({
"uri": "file:///test.rs",
"diagnostics": [{"message": "unused variable", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}}}]
}),
);
let cache = server.diagnostics.lock().expect("lock");
assert!(cache.contains_key("file:///test.rs"));
let (version, diags) = cache.get("file:///test.rs").expect("entry");
assert!(version.is_none());
assert_eq!(diags.len(), 1);
drop(cache);
let generations = server.diagnostics_generation.lock().expect("lock");
assert_eq!(generations.get("file:///test.rs").copied(), Some(1));
drop(generations);
}
#[test]
fn progress_begin_end_updates_lifecycle() {
let server = test_server();
assert!(!server.sends_progress());
assert_eq!(server.lifecycle(), ServerLifecycle::Initializing);
server.on_notification(
"$/progress",
&json!({
"token": "tok-1",
"value": { "kind": "begin", "title": "Checking", "percentage": 0 }
}),
);
assert!(server.sends_progress());
assert_eq!(server.lifecycle(), ServerLifecycle::Busy(1));
server.on_notification(
"$/progress",
&json!({
"token": "tok-2",
"value": { "kind": "begin", "title": "Indexing", "percentage": 0 }
}),
);
assert_eq!(server.lifecycle(), ServerLifecycle::Busy(2));
server.on_notification(
"$/progress",
&json!({
"token": "tok-1",
"value": { "kind": "end" }
}),
);
assert_eq!(server.lifecycle(), ServerLifecycle::Busy(1));
server.on_notification(
"$/progress",
&json!({
"token": "tok-2",
"value": { "kind": "end" }
}),
);
assert_eq!(server.lifecycle(), ServerLifecycle::Healthy);
}
#[test]
fn progress_ignored_in_terminal_state() {
let server = test_server();
server.set_lifecycle(ServerLifecycle::Dead);
server.on_notification(
"$/progress",
&json!({
"token": "tok-1",
"value": { "kind": "begin", "title": "Checking", "percentage": 0 }
}),
);
assert_eq!(server.lifecycle(), ServerLifecycle::Dead);
}
}