mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-06 18:30:35 +00:00
Move client handling from server to global static (#153)
This commit is contained in:
parent
460c1ce0a1
commit
9e13422245
3 changed files with 310 additions and 97 deletions
241
crates/djls-server/src/client.rs
Normal file
241
crates/djls-server/src/client.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(¶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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue