mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-02 00:17:56 +00:00
move all state to single field on server struct (#144)
Some checks are pending
test / tests (push) Blocked by required conditions
lint / pre-commit (push) Waiting to run
release / test (push) Waiting to run
release / release (push) Blocked by required conditions
release / build (push) Waiting to run
test / generate-matrix (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
Some checks are pending
test / tests (push) Blocked by required conditions
lint / pre-commit (push) Waiting to run
release / test (push) Waiting to run
release / release (push) Blocked by required conditions
release / build (push) Waiting to run
test / generate-matrix (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
This commit is contained in:
parent
6e4ad7ddf5
commit
00140c58ca
5 changed files with 219 additions and 157 deletions
|
@ -3,7 +3,7 @@ use djls_project::TemplateTags;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tower_lsp_server::lsp_types::*;
|
use tower_lsp_server::lsp_types::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
documents: HashMap<String, TextDocument>,
|
documents: HashMap<String, TextDocument>,
|
||||||
versions: HashMap<String, i32>,
|
versions: HashMap<String, i32>,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod documents;
|
mod documents;
|
||||||
mod queue;
|
mod queue;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod session;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
|
||||||
pub use crate::server::DjangoLanguageServer;
|
pub use crate::server::DjangoLanguageServer;
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
use crate::documents::Store;
|
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::workspace::get_project_path;
|
use crate::session::Session;
|
||||||
use djls_conf::Settings;
|
|
||||||
use djls_project::DjangoProject;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower_lsp_server::jsonrpc::Result as LspResult;
|
use tower_lsp_server::jsonrpc::Result as LspResult;
|
||||||
|
@ -14,9 +11,7 @@ const SERVER_VERSION: &str = "0.1.0";
|
||||||
|
|
||||||
pub struct DjangoLanguageServer {
|
pub struct DjangoLanguageServer {
|
||||||
client: Client,
|
client: Client,
|
||||||
project: Arc<RwLock<Option<DjangoProject>>>,
|
session: Arc<RwLock<Session>>,
|
||||||
documents: Arc<RwLock<Store>>,
|
|
||||||
settings: Arc<RwLock<Settings>>,
|
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,92 +19,32 @@ impl DjangoLanguageServer {
|
||||||
pub fn new(client: Client) -> Self {
|
pub fn new(client: Client) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
project: Arc::new(RwLock::new(None)),
|
session: Arc::new(RwLock::new(Session::default())),
|
||||||
documents: Arc::new(RwLock::new(Store::new())),
|
|
||||||
settings: Arc::new(RwLock::new(Settings::default())),
|
|
||||||
queue: Queue::new(),
|
queue: Queue::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn log_message(&self, type_: MessageType, message: &str) {
|
pub async fn with_session<R>(&self, f: impl FnOnce(&Session) -> R) -> R {
|
||||||
self.client.log_message(type_, message).await;
|
let session = self.session.read().await;
|
||||||
|
f(&session)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_settings(&self, project_path: Option<&std::path::Path>) {
|
pub async fn with_session_mut<R>(&self, f: impl FnOnce(&mut Session) -> R) -> R {
|
||||||
if let Some(path) = project_path {
|
let mut session = self.session.write().await;
|
||||||
match Settings::new(path) {
|
f(&mut session)
|
||||||
Ok(loaded_settings) => {
|
|
||||||
let mut settings_guard = self.settings.write().await;
|
|
||||||
*settings_guard = loaded_settings;
|
|
||||||
// Could potentially check if settings actually changed before logging
|
|
||||||
self.log_message(
|
|
||||||
MessageType::INFO,
|
|
||||||
&format!(
|
|
||||||
"Successfully loaded/reloaded settings for {}",
|
|
||||||
path.display()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Keep existing settings if loading/reloading fails
|
|
||||||
self.log_message(
|
|
||||||
MessageType::ERROR,
|
|
||||||
&format!(
|
|
||||||
"Failed to load/reload settings for {}: {}",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no project path, ensure we're using defaults (might already be the case)
|
|
||||||
// Or log that project-specific settings can't be loaded.
|
|
||||||
let mut settings_guard = self.settings.write().await;
|
|
||||||
*settings_guard = Settings::default(); // Reset to default if no project path
|
|
||||||
self.log_message(
|
|
||||||
MessageType::INFO,
|
|
||||||
"No project root identified. Using default settings.",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageServer for DjangoLanguageServer {
|
impl LanguageServer for DjangoLanguageServer {
|
||||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||||
self.log_message(MessageType::INFO, "Initializing server...")
|
self.client
|
||||||
|
.log_message(MessageType::INFO, "Initializing server...")
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project_path = get_project_path(¶ms);
|
self.with_session_mut(|session| {
|
||||||
|
*session.client_capabilities_mut() = Some(params.capabilities);
|
||||||
{
|
})
|
||||||
// Scope for write lock
|
.await;
|
||||||
let mut project_guard = self.project.write().await;
|
|
||||||
if let Some(ref path) = project_path {
|
|
||||||
self.log_message(
|
|
||||||
MessageType::INFO,
|
|
||||||
&format!(
|
|
||||||
"Project root identified: {}. Creating project instance.",
|
|
||||||
path.display()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
*project_guard = Some(DjangoProject::new(path.clone()));
|
|
||||||
} else {
|
|
||||||
self.log_message(
|
|
||||||
MessageType::WARNING,
|
|
||||||
"Could not determine project root. Project features will be unavailable.",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
// Ensure it's None if no path
|
|
||||||
*project_guard = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_settings(project_path.as_deref()).await;
|
|
||||||
|
|
||||||
Ok(InitializeResult {
|
Ok(InitializeResult {
|
||||||
capabilities: ServerCapabilities {
|
capabilities: ServerCapabilities {
|
||||||
|
@ -127,7 +62,6 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
supported: Some(true),
|
supported: Some(true),
|
||||||
change_notifications: Some(OneOf::Left(true)),
|
change_notifications: Some(OneOf::Left(true)),
|
||||||
}),
|
}),
|
||||||
// Add file operations if needed later
|
|
||||||
file_operations: None,
|
file_operations: None,
|
||||||
}),
|
}),
|
||||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||||
|
@ -150,22 +84,68 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn initialized(&self, _params: InitializedParams) {
|
async fn initialized(&self, _params: InitializedParams) {
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::INFO,
|
.log_message(
|
||||||
"Server received initialized notification.",
|
MessageType::INFO,
|
||||||
)
|
"Server received initialized notification.",
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let project_arc = Arc::clone(&self.project);
|
let init_params = InitializeParams {
|
||||||
|
// Using the current directory by default right now, but we should switch to
|
||||||
|
// *falling back* to current dir if workspace folders is empty
|
||||||
|
workspace_folders: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let has_project =
|
||||||
|
if let Some(project_path) = crate::workspace::get_project_path(&init_params) {
|
||||||
|
self.with_session_mut(|session| {
|
||||||
|
let settings = djls_conf::Settings::new(&project_path)
|
||||||
|
.unwrap_or_else(|_| djls_conf::Settings::default());
|
||||||
|
*session.settings_mut() = settings;
|
||||||
|
|
||||||
|
*session.project_mut() = Some(djls_project::DjangoProject::new(project_path));
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_project {
|
||||||
|
self.client
|
||||||
|
.log_message(
|
||||||
|
MessageType::INFO,
|
||||||
|
"Project discovered from current directory",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.client
|
||||||
|
.log_message(
|
||||||
|
MessageType::INFO,
|
||||||
|
"No project discovered; running without project context",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_arc = Arc::clone(&self.session);
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let settings_arc = Arc::clone(&self.settings);
|
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.queue
|
.queue
|
||||||
.submit(async move {
|
.submit(async move {
|
||||||
let mut project_guard = project_arc.write().await;
|
let project_path_and_venv = {
|
||||||
if let Some(project) = project_guard.as_mut() {
|
let session = session_arc.read().await;
|
||||||
let path_display = project.path().display().to_string();
|
session.project().map(|p| {
|
||||||
|
(
|
||||||
|
p.path().display().to_string(),
|
||||||
|
session.settings().venv_path().map(|s| s.to_string()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((path_display, venv_path)) = project_path_and_venv {
|
||||||
client
|
client
|
||||||
.log_message(
|
.log_message(
|
||||||
MessageType::INFO,
|
MessageType::INFO,
|
||||||
|
@ -176,11 +156,6 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let venv_path = {
|
|
||||||
let settings = settings_arc.read().await;
|
|
||||||
settings.venv_path().map(|s| s.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(ref path) = venv_path {
|
if let Some(ref path) = venv_path {
|
||||||
client
|
client
|
||||||
.log_message(
|
.log_message(
|
||||||
|
@ -190,7 +165,17 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
match project.initialize(venv_path.as_deref()) {
|
let init_result = {
|
||||||
|
let mut session = session_arc.write().await;
|
||||||
|
if let Some(project) = session.project_mut().as_mut() {
|
||||||
|
project.initialize(venv_path.as_deref())
|
||||||
|
} else {
|
||||||
|
// Project was removed between read and write locks
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match init_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
client
|
client
|
||||||
.log_message(
|
.log_message(
|
||||||
|
@ -212,7 +197,10 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
*project_guard = None;
|
|
||||||
|
// Clear project on error
|
||||||
|
let mut session = session_arc.write().await;
|
||||||
|
*session.project_mut() = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -227,13 +215,15 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::ERROR,
|
.log_message(
|
||||||
&format!("Failed to submit project initialization task: {}", e),
|
MessageType::ERROR,
|
||||||
)
|
&format!("Failed to submit project initialization task: {}", e),
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
self.log_message(MessageType::INFO, "Scheduled project initialization task.")
|
self.client
|
||||||
|
.log_message(MessageType::INFO, "Scheduled project initialization task.")
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,82 +233,98 @@ impl LanguageServer for DjangoLanguageServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||||
if let Err(e) = self.documents.write().await.handle_did_open(params.clone()) {
|
let result = self
|
||||||
|
.with_session_mut(|session| session.documents_mut().handle_did_open(params.clone()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
eprintln!("Error handling document open: {}", e);
|
eprintln!("Error handling document open: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::INFO,
|
.log_message(
|
||||||
&format!("Opened document: {:?}", params.text_document.uri),
|
MessageType::INFO,
|
||||||
)
|
&format!("Opened document: {:?}", params.text_document.uri),
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||||
if let Err(e) = self
|
let result = self
|
||||||
.documents
|
.with_session_mut(|session| session.documents_mut().handle_did_change(params.clone()))
|
||||||
.write()
|
.await;
|
||||||
.await
|
|
||||||
.handle_did_change(params.clone())
|
if let Err(e) = result {
|
||||||
{
|
|
||||||
eprintln!("Error handling document change: {}", e);
|
eprintln!("Error handling document change: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::INFO,
|
.log_message(
|
||||||
&format!("Changed document: {:?}", params.text_document.uri),
|
MessageType::INFO,
|
||||||
)
|
&format!("Changed document: {:?}", params.text_document.uri),
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||||
if let Err(e) = self
|
let result = self
|
||||||
.documents
|
.with_session_mut(|session| session.documents_mut().handle_did_close(params.clone()))
|
||||||
.write()
|
.await;
|
||||||
.await
|
|
||||||
.handle_did_close(params.clone())
|
if let Err(e) = result {
|
||||||
{
|
|
||||||
eprintln!("Error handling document close: {}", e);
|
eprintln!("Error handling document close: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::INFO,
|
.log_message(
|
||||||
&format!("Closed document: {:?}", params.text_document.uri),
|
MessageType::INFO,
|
||||||
)
|
&format!("Closed document: {:?}", params.text_document.uri),
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
|
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
|
||||||
let project_guard = self.project.read().await;
|
Ok(self
|
||||||
let documents_guard = self.documents.read().await;
|
.with_session(|session| {
|
||||||
|
if let Some(project) = session.project() {
|
||||||
if let Some(project) = project_guard.as_ref() {
|
if let Some(tags) = project.template_tags() {
|
||||||
if let Some(tags) = project.template_tags() {
|
return session.documents().get_completions(
|
||||||
return Ok(documents_guard.get_completions(
|
params.text_document_position.text_document.uri.as_str(),
|
||||||
params.text_document_position.text_document.uri.as_str(),
|
params.text_document_position.position,
|
||||||
params.text_document_position.position,
|
tags,
|
||||||
tags,
|
);
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
}
|
None
|
||||||
Ok(None)
|
})
|
||||||
|
.await)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
|
async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
|
||||||
self.log_message(
|
self.client
|
||||||
MessageType::INFO,
|
.log_message(
|
||||||
"Configuration change detected. Reloading settings...",
|
MessageType::INFO,
|
||||||
)
|
"Configuration change detected. Reloading settings...",
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let project_path = {
|
let project_path = self
|
||||||
let project_guard = self.project.read().await;
|
.with_session(|session| session.project().map(|p| p.path().to_path_buf()))
|
||||||
project_guard.as_ref().map(|p| p.path().to_path_buf())
|
.await;
|
||||||
};
|
|
||||||
|
|
||||||
self.update_settings(project_path.as_deref()).await;
|
if let Some(path) = project_path {
|
||||||
|
self.with_session_mut(|session| match djls_conf::Settings::new(path.as_path()) {
|
||||||
|
Ok(new_settings) => {
|
||||||
|
*session.settings_mut() = new_settings;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading settings: {}", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
55
crates/djls-server/src/session.rs
Normal file
55
crates/djls-server/src/session.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::documents::Store;
|
||||||
|
use djls_conf::Settings;
|
||||||
|
use djls_project::DjangoProject;
|
||||||
|
use tower_lsp_server::lsp_types::ClientCapabilities;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Session {
|
||||||
|
client_capabilities: Option<ClientCapabilities>,
|
||||||
|
project: Option<DjangoProject>,
|
||||||
|
documents: Store,
|
||||||
|
settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(client_capabilities: ClientCapabilities) -> Self {
|
||||||
|
Self {
|
||||||
|
client_capabilities: Some(client_capabilities),
|
||||||
|
project: None,
|
||||||
|
documents: Store::new(),
|
||||||
|
settings: Settings::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_capabilities(&self) -> &Option<ClientCapabilities> {
|
||||||
|
&self.client_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_capabilities_mut(&mut self) -> &mut Option<ClientCapabilities> {
|
||||||
|
&mut self.client_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn project(&self) -> Option<&DjangoProject> {
|
||||||
|
self.project.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn project_mut(&mut self) -> &mut Option<DjangoProject> {
|
||||||
|
&mut self.project
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn documents(&self) -> &Store {
|
||||||
|
&self.documents
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn documents_mut(&mut self) -> &mut Store {
|
||||||
|
&mut self.documents
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings(&self) -> &Settings {
|
||||||
|
&self.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings_mut(&mut self) -> &mut Settings {
|
||||||
|
&mut self.settings
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ pub fn get_project_path(params: &InitializeParams) -> Option<PathBuf> {
|
||||||
/// Converts a `file:` URI into an absolute `PathBuf`.
|
/// Converts a `file:` URI into an absolute `PathBuf`.
|
||||||
fn uri_to_pathbuf(uri: &Uri) -> Option<PathBuf> {
|
fn uri_to_pathbuf(uri: &Uri) -> Option<PathBuf> {
|
||||||
// Check if the scheme is "file"
|
// Check if the scheme is "file"
|
||||||
if uri.scheme().map_or(true, |s| s.as_str() != "file") {
|
if uri.scheme().is_none_or(|s| s.as_str() != "file") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue