This commit is contained in:
Josh Thomas 2025-05-16 21:36:26 -05:00
parent 460c1ce0a1
commit 414808e502
3 changed files with 394 additions and 97 deletions

View file

@ -0,0 +1,322 @@
use std::future::Future;
use std::sync::{
atomic::{AtomicU8, Ordering},
Arc, OnceLock,
};
use tower_lsp_server::jsonrpc::Error;
use tower_lsp_server::lsp_types::notification::Notification;
use tower_lsp_server::lsp_types::{Diagnostic, MessageType, NumberOrString, Uri};
use tower_lsp_server::Client;
pub 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");
}
/// Run an async operation with the client if available
///
/// This helper function encapsulates the common pattern of checking if the client
/// is available, then spawning a task to run an async operation with it.
fn with_client<F, Fut>(f: F)
where
F: FnOnce(Arc<Client>) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
if let Some(client) = CLIENT.get().cloned() {
tokio::spawn(async move {
f(client).await;
});
}
}
pub fn log_message(message_type: MessageType, message: &str) {
let message = message.to_string();
with_client(move |client| async move {
client.log_message(message_type, &message).await;
});
}
pub fn show_message(message_type: MessageType, message: &str) {
let message = message.to_string();
with_client(move |client| async move {
client.show_message(message_type, &message).await;
});
}
pub fn publish_diagnostics(uri: &str, diagnostics: Vec<Diagnostic>, version: Option<i32>) {
let uri = match uri.parse::<Uri>() {
Ok(uri) => uri,
Err(e) => {
eprintln!("Invalid URI for diagnostics: {uri} - {e}");
return;
}
};
with_client(move |client| async move {
client.publish_diagnostics(uri, diagnostics, version).await;
});
}
pub fn send_notification<N>(params: N::Params)
where
N: Notification,
N::Params: Send + 'static,
{
with_client(move |client| async move {
client.send_notification::<N>(params).await;
});
}
/// Start progress reporting
pub fn start_progress(
token: impl Into<NumberOrString> + Send + 'static,
title: &str,
message: Option<String>,
) {
let token = token.into();
let title = title.to_string();
with_client(move |client| async move {
let progress = client.progress(token, title);
// Add optional message if provided
let progress = if let Some(msg) = message {
progress.with_message(msg)
} else {
progress
};
// Begin the progress reporting
let _ = progress.begin().await;
});
}
/// Report progress
pub fn report_progress(
token: impl Into<NumberOrString> + Send + 'static,
title: &str,
message: Option<String>,
percentage: Option<u32>,
) {
let token = token.into();
let title = title.to_string();
with_client(move |client| async move {
// First begin the progress
let ongoing_progress = client.progress(token, title).begin().await;
match (message, percentage) {
(Some(msg), Some(_pct)) => {
// Both message and percentage - can't easily represent both with unbounded
ongoing_progress.report(msg).await;
}
(Some(msg), None) => {
// Only message
ongoing_progress.report(msg).await;
}
(None, Some(_pct)) => {
// Only percentage - not supported in unbounded progress
// We'd need to use bounded progress with percentage
}
(None, None) => {
// Nothing to report
}
}
});
}
/// End progress reporting
pub fn end_progress(
token: impl Into<NumberOrString> + Send + 'static,
title: &str,
message: Option<String>,
) {
let token = token.into();
let title = title.to_string();
with_client(move |client| async move {
let ongoing_progress = client.progress(token, title).begin().await;
if let Some(msg) = message {
ongoing_progress.finish_with_message(msg).await;
} else {
ongoing_progress.finish().await;
}
});
}
/// States for progress tracking
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressState {
NotStarted = 0,
Started = 1,
Finished = 2,
}
/// A handle for managing progress reporting with lifecycle tracking
#[derive(Clone)]
pub struct ProgressHandle {
client: Arc<Client>,
token: NumberOrString,
title: String,
state: Arc<AtomicU8>,
}
impl ProgressHandle {
/// Create a new progress operation and return a handle to it
pub fn new(token: impl Into<NumberOrString>, title: &str) -> Option<Self> {
let token = token.into();
let title = title.to_string();
CLIENT.get().cloned().map(|client| {
// Create the handle
let handle = Self {
client: client.clone(),
token: token.clone(),
title: title.clone(),
state: Arc::new(AtomicU8::new(ProgressState::NotStarted as u8)),
};
// Clone for the closure
let handle_clone = handle.clone();
// Start the progress and update state
tokio::spawn(async move {
let _ = client.progress(token, title).begin().await;
handle_clone.update_state(ProgressState::Started);
});
handle
})
}
/// Update the progress state
fn update_state(&self, state: ProgressState) {
self.state.store(state as u8, Ordering::SeqCst);
}
/// Get the current progress state
pub fn state(&self) -> ProgressState {
match self.state.load(Ordering::SeqCst) {
0 => ProgressState::NotStarted,
1 => ProgressState::Started,
_ => ProgressState::Finished,
}
}
/// Check if already finished - returns true if already finished
fn is_finished(&self) -> bool {
self.state
.swap(ProgressState::Finished as u8, Ordering::SeqCst)
== ProgressState::Finished as u8
}
/// Report progress with a message
pub fn report(&self, message: impl Into<String>) {
// Only report if in Started state
if self.state() != ProgressState::Started {
return;
}
let token = self.token.clone();
let title = self.title.clone();
let message = message.into();
let client = self.client.clone();
tokio::spawn(async move {
let ongoing = client.progress(token, title).begin().await;
ongoing.report(message).await;
});
}
/// Complete the progress
pub fn finish(self, message: impl Into<String>) {
// Only finish if not already finished
if self.is_finished() {
return;
}
let token = self.token.clone();
let title = self.title.clone();
let message = message.into();
let client = self.client.clone();
tokio::spawn(async move {
let ongoing = client.progress(token, title).begin().await;
ongoing.finish_with_message(message).await;
});
}
/// Complete the progress without a message
pub fn finish_without_message(self) {
// Only finish if not already finished
if self.is_finished() {
return;
}
let client = self.client.clone();
let token = self.token.clone();
let title = self.title.clone();
tokio::spawn(async move {
let ongoing = client.progress(token, title).begin().await;
ongoing.finish().await;
});
}
}
/// Send a custom request to the client using a specific LSP request type
///
/// Unlike other methods, this one needs to be async since it returns a result.
pub async fn send_request<R>(params: R::Params) -> Result<R::Result, Error>
where
R: tower_lsp_server::lsp_types::request::Request,
R::Params: Send,
R::Result: Send,
{
if let Some(client) = CLIENT.get() {
client.send_request::<R>(params).await
} else {
Err(Error::internal_error())
}
}
/// Show an info message in the client's UI
pub fn info(message: &str) {
show_message(MessageType::INFO, message);
}
/// Show a warning message in the client's UI
pub fn warn(message: &str) {
show_message(MessageType::WARNING, message);
}
/// Show an error message in the client's UI
pub fn error(message: &str) {
show_message(MessageType::ERROR, message);
}
/// Log an info message to the client's log
pub fn log_info(message: &str) {
log_message(MessageType::INFO, message);
}
/// Log a warning message to the client's log
pub fn log_warn(message: &str) {
log_message(MessageType::WARNING, message);
}
/// Log an error message to the client's log
pub fn log_error(message: &str) {
log_message(MessageType::ERROR, message);
}
/// Clear all diagnostics for a file by publishing an empty array
pub fn clear_diagnostics(uri: &str) {
publish_diagnostics(uri, vec![], None);
}

View file

@ -1,3 +1,4 @@
mod client;
mod db;
mod documents;
mod queue;
@ -20,7 +21,12 @@ 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| {
// Initialize the global client
client::init_client(client);
// Then create the server instance
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,9 @@ impl LanguageServer for DjangoLanguageServer {
*session.settings_mut() = new_settings;
}
Err(e) => {
eprintln!("Error loading settings: {e}");
let error_msg = format!("Error loading settings: {e}");
eprintln!("{error_msg}");
client::log_message(MessageType::ERROR, &error_msg);
}
})
.await;