Move client handling from server to global static (#153)

This commit is contained in:
Josh Thomas 2025-05-17 19:24:55 -05:00 committed by GitHub
parent 460c1ce0a1
commit 9e13422245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 310 additions and 97 deletions

View file

@ -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<Arc<Client>> = 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<Arc<Client>> {
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<T, Error>` 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<bool, Error> {
/// 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<Vec<MessageActionItem>> ; Option<MessageActionItem>);
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<Diagnostic>, version: Option<i32>);
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<ConfigurationItem> ; Vec<LSPAny>);
request!(workspace_folders, ; Option<Vec<WorkspaceFolder>>);
}
#[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<Registration>);
notify_discard!(unregister_capability, unregisterations: Vec<Unregistration>);
}
#[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<S: Serialize + Send + 'static>(data: S) {
if let Some(client) = get_client() {
tokio::spawn(async move {
client.telemetry_event(data).await;
});
}
}
pub fn progress<T: Into<String> + Send>(token: ProgressToken, title: T) -> Option<Progress> {
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<N>(params: N::Params)
where
N: Notification,
N::Params: Send + 'static,
{
if let Some(client) = get_client() {
tokio::spawn(async move {
client.send_notification::<N>(params).await;
});
}
}
pub async fn send_request<R>(params: R::Params) -> Result<R::Result, Error>
where
R: Request,
R::Params: Send + 'static,
R::Result: Send + 'static,
{
if let Some(client) = get_client() {
client.send_request::<R>(params).await
} else {
Err(Error::internal_error())
}
}
}

View file

@ -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;

View file

@ -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<RwLock<Session>>,
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<InitializeResult> {
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(&params);
@ -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;