diff --git a/crates/djls-server/src/client.rs b/crates/djls-server/src/client.rs new file mode 100644 index 0000000..35eb841 --- /dev/null +++ b/crates/djls-server/src/client.rs @@ -0,0 +1,241 @@ +use std::fmt::Display; +use std::sync::Arc; +use std::sync::OnceLock; + +pub use messages::*; +use tower_lsp_server::jsonrpc::Error; +use tower_lsp_server::Client; + +static CLIENT: OnceLock> = OnceLock::new(); + +pub fn init_client(client: Client) { + let client_arc = Arc::new(client); + CLIENT + .set(client_arc) + .expect("client should only be initialized once"); +} + +fn get_client() -> Option> { + CLIENT.get().cloned() +} + +/// Generates a fire-and-forget notification function that spawns an async task. +/// +/// This macro creates a wrapper function that: +/// 1. Gets the global client instance +/// 2. Spawns a new Tokio task that calls the client method asynchronously +/// 3. Does not wait for completion or handle errors +/// +/// This... +/// ```rust,ignore +/// notify!(log_message, message_type: MessageType, message: impl Display + Send + 'static); +/// ``` +/// +/// ...expands to: +/// ```rust,ignore +/// pub fn log_message(message_type: MessageType, message: impl Display + Send + 'static) { +/// if let Some(client) = get_client() { +/// tokio::spawn(async move { +/// client.log_message(message_type, message).await; +/// }); +/// } +/// } +/// ``` +macro_rules! notify { + ($name:ident, $($param:ident: $type:ty),*) => { + pub fn $name($($param: $type),*) { + if let Some(client) = get_client() { + tokio::spawn(async move { + client.$name($($param),*).await; + }); + } + } + }; +} + +/// Generates a fire-and-forget notification function that spawns an async task and discards any errors. +/// +/// Similar to `notify!`, but explicitly discards any errors returned by the client method. +/// This is useful for methods that might return a Result but where you don't care about the outcome. +/// +/// This... +/// ```rust,ignore +/// notify_discard!(code_lens_refresh,); +/// ``` +/// +/// ...expands to: +/// ```rust,ignore +/// pub fn code_lens_refresh() { +/// if let Some(client) = get_client() { +/// tokio::spawn(async move { +/// let _ = client.code_lens_refresh().await; +/// }); +/// } +/// } +/// ``` +macro_rules! notify_discard { + ($name:ident, $($param:ident: $type:ty),*) => { + pub fn $name($($param: $type),*) { + if let Some(client) = get_client() { + tokio::spawn(async move { + let _ = client.$name($($param),*).await; + }); + } + } + }; +} + +/// Generates an async request function that awaits a response from the client. +/// +/// Unlike the notification macros, this creates a function that: +/// 1. Is marked as `async` and must be awaited +/// 2. Returns a `Result` with the response type +/// 3. Fails with an internal error if the client is not available +/// +/// The semi-colon (`;`) separates the parameters from the return type. +/// +/// This... +/// ```rust,ignore +/// request!(show_document, params: ShowDocumentParams ; bool); +/// ``` +/// +/// ...expands to: +/// ```rust,ignore +/// pub async fn show_document(params: ShowDocumentParams) -> Result { +/// if let Some(client) = get_client() { +/// client.show_document(params).await +/// } else { +/// Err(Error::internal_error()) +/// } +/// } +/// ``` +macro_rules! request { + ($name:ident, $($param:ident: $type:ty),* ; $result:ty) => { + pub async fn $name($($param: $type),*) -> Result<$result, Error> { + if let Some(client) = get_client() { + client.$name($($param),*).await + } else { + Err(Error::internal_error()) + } + } + }; +} + +#[allow(dead_code)] +pub mod messages { + use tower_lsp_server::lsp_types::MessageActionItem; + use tower_lsp_server::lsp_types::MessageType; + use tower_lsp_server::lsp_types::ShowDocumentParams; + + use super::get_client; + use super::Display; + use super::Error; + + notify!(log_message, message_type: MessageType, message: impl Display + Send + 'static); + notify!(show_message, message_type: MessageType, message: impl Display + Send + 'static); + request!(show_message_request, message_type: MessageType, message: impl Display + Send + 'static, actions: Option> ; Option); + request!(show_document, params: ShowDocumentParams ; bool); +} + +#[allow(dead_code)] +pub mod diagnostics { + use tower_lsp_server::lsp_types::Diagnostic; + use tower_lsp_server::lsp_types::Uri; + + use super::get_client; + + notify!(publish_diagnostics, uri: Uri, diagnostics: Vec, version: Option); + notify_discard!(workspace_diagnostic_refresh,); +} + +#[allow(dead_code)] +pub mod workspace { + use tower_lsp_server::lsp_types::ApplyWorkspaceEditResponse; + use tower_lsp_server::lsp_types::ConfigurationItem; + use tower_lsp_server::lsp_types::LSPAny; + use tower_lsp_server::lsp_types::WorkspaceEdit; + use tower_lsp_server::lsp_types::WorkspaceFolder; + + use super::get_client; + use super::Error; + + request!(apply_edit, edit: WorkspaceEdit ; ApplyWorkspaceEditResponse); + request!(configuration, items: Vec ; Vec); + request!(workspace_folders, ; Option>); +} + +#[allow(dead_code)] +pub mod editor { + use super::get_client; + + notify_discard!(code_lens_refresh,); + notify_discard!(semantic_tokens_refresh,); + notify_discard!(inline_value_refresh,); + notify_discard!(inlay_hint_refresh,); +} + +#[allow(dead_code)] +pub mod capabilities { + use tower_lsp_server::lsp_types::Registration; + use tower_lsp_server::lsp_types::Unregistration; + + use super::get_client; + + notify_discard!(register_capability, registrations: Vec); + notify_discard!(unregister_capability, unregisterations: Vec); +} + +#[allow(dead_code)] +pub mod monitoring { + use serde::Serialize; + use tower_lsp_server::lsp_types::ProgressToken; + use tower_lsp_server::Progress; + + use super::get_client; + + pub fn telemetry_event(data: S) { + if let Some(client) = get_client() { + tokio::spawn(async move { + client.telemetry_event(data).await; + }); + } + } + + pub fn progress + Send>(token: ProgressToken, title: T) -> Option { + get_client().map(|client| client.progress(token, title)) + } +} + +#[allow(dead_code)] +pub mod protocol { + use tower_lsp_server::lsp_types::notification::Notification; + use tower_lsp_server::lsp_types::request::Request; + + use super::get_client; + use super::Error; + + pub fn send_notification(params: N::Params) + where + N: Notification, + N::Params: Send + 'static, + { + if let Some(client) = get_client() { + tokio::spawn(async move { + client.send_notification::(params).await; + }); + } + } + + pub async fn send_request(params: R::Params) -> Result + where + R: Request, + R::Params: Send + 'static, + R::Result: Send + 'static, + { + if let Some(client) = get_client() { + client.send_request::(params).await + } else { + Err(Error::internal_error()) + } + } +} diff --git a/crates/djls-server/src/lib.rs b/crates/djls-server/src/lib.rs index 99c0ee3..acb1d6c 100644 --- a/crates/djls-server/src/lib.rs +++ b/crates/djls-server/src/lib.rs @@ -1,3 +1,4 @@ +mod client; mod db; mod documents; mod queue; @@ -20,7 +21,11 @@ pub fn run() -> Result<()> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = LspService::build(DjangoLanguageServer::new).finish(); + let (service, socket) = LspService::build(|client| { + client::init_client(client); + DjangoLanguageServer::new() + }) + .finish(); Server::new(stdin, stdout, socket).serve(service).await; diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 9d5c40e..31bd723 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -22,9 +22,9 @@ use tower_lsp_server::lsp_types::TextDocumentSyncKind; use tower_lsp_server::lsp_types::TextDocumentSyncOptions; use tower_lsp_server::lsp_types::WorkspaceFoldersServerCapabilities; use tower_lsp_server::lsp_types::WorkspaceServerCapabilities; -use tower_lsp_server::Client; use tower_lsp_server::LanguageServer; +use crate::client; use crate::queue::Queue; use crate::session::Session; @@ -32,16 +32,14 @@ const SERVER_NAME: &str = "Django Language Server"; const SERVER_VERSION: &str = "0.1.0"; pub struct DjangoLanguageServer { - client: Client, session: Arc>, queue: Queue, } impl DjangoLanguageServer { #[must_use] - pub fn new(client: Client) -> Self { + pub fn new() -> Self { Self { - client, session: Arc::new(RwLock::new(Session::default())), queue: Queue::new(), } @@ -60,9 +58,7 @@ impl DjangoLanguageServer { impl LanguageServer for DjangoLanguageServer { async fn initialize(&self, params: InitializeParams) -> LspResult { - self.client - .log_message(MessageType::INFO, "Initializing server...") - .await; + client::log_message(MessageType::INFO, "Initializing server..."); self.with_session_mut(|session| { *session.client_capabilities_mut() = Some(params.capabilities); @@ -108,12 +104,10 @@ impl LanguageServer for DjangoLanguageServer { #[allow(clippy::too_many_lines)] async fn initialized(&self, _params: InitializedParams) { - self.client - .log_message( - MessageType::INFO, - "Server received initialized notification.", - ) - .await; + client::log_message( + MessageType::INFO, + "Server received initialized notification.", + ); let init_params = InitializeParams { // Using the current directory by default right now, but we should switch to @@ -138,23 +132,18 @@ impl LanguageServer for DjangoLanguageServer { }; if has_project { - self.client - .log_message( - MessageType::INFO, - "Project discovered from current directory", - ) - .await; + client::log_message( + MessageType::INFO, + "Project discovered from current directory", + ); } else { - self.client - .log_message( - MessageType::INFO, - "No project discovered; running without project context", - ) - .await; + client::log_message( + MessageType::INFO, + "No project discovered; running without project context", + ); } let session_arc = Arc::clone(&self.session); - let client = self.client.clone(); if let Err(e) = self .queue @@ -170,22 +159,18 @@ impl LanguageServer for DjangoLanguageServer { }; if let Some((path_display, venv_path)) = project_path_and_venv { - client - .log_message( - MessageType::INFO, - &format!( - "Task: Starting initialization for project at: {path_display}" - ), - ) - .await; + client::log_message( + MessageType::INFO, + format!( + "Task: Starting initialization for project at: {path_display}" + ), + ); if let Some(ref path) = venv_path { - client - .log_message( - MessageType::INFO, - &format!("Using virtual environment from config: {path}"), - ) - .await; + client::log_message( + MessageType::INFO, + format!("Using virtual environment from config: {path}"), + ); } let init_result = { @@ -200,24 +185,20 @@ impl LanguageServer for DjangoLanguageServer { match init_result { Ok(()) => { - client - .log_message( - MessageType::INFO, - &format!( - "Task: Successfully initialized project: {path_display}" - ), - ) - .await; + client::log_message( + MessageType::INFO, + format!( + "Task: Successfully initialized project: {path_display}" + ), + ); } Err(e) => { - client - .log_message( - MessageType::ERROR, - &format!( - "Task: Failed to initialize Django project at {path_display}: {e}" - ), - ) - .await; + client::log_message( + MessageType::ERROR, + format!( + "Task: Failed to initialize Django project at {path_display}: {e}" + ), + ); // Clear project on error let mut session = session_arc.write().await; @@ -225,27 +206,21 @@ impl LanguageServer for DjangoLanguageServer { } } } else { - client - .log_message( - MessageType::INFO, - "Task: No project instance found to initialize.", - ) - .await; + client::log_message( + MessageType::INFO, + "Task: No project instance found to initialize.", + ); } Ok(()) }) .await { - self.client - .log_message( - MessageType::ERROR, - &format!("Failed to submit project initialization task: {e}"), - ) - .await; + client::log_message( + MessageType::ERROR, + format!("Failed to submit project initialization task: {e}"), + ); } else { - self.client - .log_message(MessageType::INFO, "Scheduled project initialization task.") - .await; + client::log_message(MessageType::INFO, "Scheduled project initialization task."); } } @@ -254,12 +229,10 @@ impl LanguageServer for DjangoLanguageServer { } async fn did_open(&self, params: DidOpenTextDocumentParams) { - self.client - .log_message( - MessageType::INFO, - &format!("Opened document: {:?}", params.text_document.uri), - ) - .await; + client::log_message( + MessageType::INFO, + format!("Opened document: {:?}", params.text_document.uri), + ); self.with_session_mut(|session| { let db = session.db(); @@ -269,12 +242,10 @@ impl LanguageServer for DjangoLanguageServer { } async fn did_change(&self, params: DidChangeTextDocumentParams) { - self.client - .log_message( - MessageType::INFO, - &format!("Changed document: {:?}", params.text_document.uri), - ) - .await; + client::log_message( + MessageType::INFO, + format!("Changed document: {:?}", params.text_document.uri), + ); self.with_session_mut(|session| { let db = session.db(); @@ -284,12 +255,10 @@ impl LanguageServer for DjangoLanguageServer { } async fn did_close(&self, params: DidCloseTextDocumentParams) { - self.client - .log_message( - MessageType::INFO, - &format!("Closed document: {:?}", params.text_document.uri), - ) - .await; + client::log_message( + MessageType::INFO, + format!("Closed document: {:?}", params.text_document.uri), + ); self.with_session_mut(|session| { session.documents_mut().handle_did_close(¶ms); @@ -317,12 +286,10 @@ impl LanguageServer for DjangoLanguageServer { } async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) { - self.client - .log_message( - MessageType::INFO, - "Configuration change detected. Reloading settings...", - ) - .await; + client::log_message( + MessageType::INFO, + "Configuration change detected. Reloading settings...", + ); let project_path = self .with_session(|session| session.project().map(|p| p.path().to_path_buf())) @@ -334,7 +301,7 @@ impl LanguageServer for DjangoLanguageServer { *session.settings_mut() = new_settings; } Err(e) => { - eprintln!("Error loading settings: {e}"); + client::log_message(MessageType::ERROR, format!("Error loading settings: {e}")); } }) .await;