mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-08-04 10:08:30 +00:00
wip
This commit is contained in:
parent
460c1ce0a1
commit
414808e502
3 changed files with 394 additions and 97 deletions
322
crates/djls-server/src/client.rs
Normal file
322
crates/djls-server/src/client.rs
Normal 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);
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue