This commit is contained in:
Josh Thomas 2025-08-22 14:18:39 -05:00
parent 66cc577569
commit cabb77f67d
8 changed files with 419 additions and 2 deletions

View file

@ -13,6 +13,7 @@ djls-project = { workspace = true }
djls-templates = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
percent-encoding = { workspace = true }
pyo3 = { workspace = true }
salsa = { workspace = true }

View file

@ -2,6 +2,7 @@ use std::future::Future;
use std::sync::Arc;
use tokio::sync::RwLock;
use serde_json;
use tower_lsp_server::jsonrpc::Result as LspResult;
use tower_lsp_server::lsp_types::CompletionOptions;
use tower_lsp_server::lsp_types::CompletionParams;
@ -11,6 +12,8 @@ use tower_lsp_server::lsp_types::DidChangeTextDocumentParams;
use tower_lsp_server::lsp_types::DidCloseTextDocumentParams;
use tower_lsp_server::lsp_types::DidOpenTextDocumentParams;
use tower_lsp_server::lsp_types::DidSaveTextDocumentParams;
use tower_lsp_server::lsp_types::ExecuteCommandOptions;
use tower_lsp_server::lsp_types::ExecuteCommandParams;
use tower_lsp_server::lsp_types::InitializeParams;
use tower_lsp_server::lsp_types::InitializeResult;
use tower_lsp_server::lsp_types::InitializedParams;
@ -129,6 +132,10 @@ impl LanguageServer for DjangoLanguageServer {
save: Some(SaveOptions::default().into()),
},
)),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec!["djls/dumpState".to_string()],
..Default::default()
}),
..Default::default()
},
server_info: Some(ServerInfo {
@ -274,6 +281,35 @@ impl LanguageServer for DjangoLanguageServer {
.await)
}
async fn execute_command(&self, params: ExecuteCommandParams) -> LspResult<Option<serde_json::Value>> {
match params.command.as_str() {
"djls/dumpState" => {
tracing::info!("Executing djls/dumpState command");
// Check if debug mode is enabled
if std::env::var("DJLS_DEBUG").is_err() {
tracing::warn!("djls/dumpState command requires DJLS_DEBUG environment variable");
return Ok(Some(serde_json::json!({
"error": "Debug mode not enabled. Set DJLS_DEBUG environment variable."
})));
}
let result = self.with_session(|session| {
session.dump_debug_state()
}).await;
Ok(Some(serde_json::json!({
"success": true,
"message": result
})))
}
_ => {
tracing::warn!("Unknown command: {}", params.command);
Ok(None)
}
}
}
async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
tracing::info!("Configuration change detected. Reloading settings...");

View file

@ -3,6 +3,7 @@ use djls_project::DjangoProject;
use salsa::StorageHandle;
use tower_lsp_server::lsp_types::ClientCapabilities;
use tower_lsp_server::lsp_types::InitializeParams;
use serde_json;
use crate::db::ServerDatabase;
use crate::workspace::Store;
@ -103,4 +104,34 @@ impl Session {
let storage = self.db_handle.clone().into_storage();
ServerDatabase::new(storage)
}
/// Dump debug state to file for development/debugging purposes
pub fn dump_debug_state(&self) -> String {
use std::fs;
use chrono::{DateTime, Utc};
let timestamp: DateTime<Utc> = Utc::now();
let filename = format!("djls-debug-state-{}.json", timestamp.format("%Y%m%d-%H%M%S"));
let debug_info = serde_json::json!({
"timestamp": timestamp.to_rfc3339(),
"vfs": self.documents.debug_vfs_state(),
"store": self.documents.debug_store_state(),
"project": {
"path": self.project.as_ref().map(|p| p.path().display().to_string()),
"has_project": self.project.is_some()
}
});
match fs::write(&filename, serde_json::to_string_pretty(&debug_info).unwrap_or_else(|_| "{}".to_string())) {
Ok(_) => {
tracing::info!("Debug state dumped to: {}", filename);
format!("Debug state written to: {}", filename)
}
Err(e) => {
tracing::error!("Failed to write debug state: {}", e);
format!("Failed to write debug state: {}", e)
}
}
}
}

View file

@ -176,6 +176,16 @@ impl LineIndex {
Position::new(u32::try_from(line).unwrap_or(0), character)
}
/// Get line start offset for a given line number
pub fn line_start(&self, line: usize) -> Option<u32> {
self.line_starts.get(line).copied()
}
/// Get total content length
pub fn length(&self) -> u32 {
self.length
}
}
#[derive(Clone, Debug, PartialEq)]

View file

@ -1,6 +1,7 @@
use super::fs::FileSystem;
use std::collections::HashMap;
use std::path::PathBuf;
use serde_json;
use anyhow::anyhow;
use anyhow::Result;
@ -23,6 +24,7 @@ use tower_lsp_server::lsp_types::Position;
use super::document::ClosingBrace;
use super::document::LanguageId;
use super::document::LineIndex;
use super::document::TemplateTagContext;
use super::document::TextDocument;
use super::utils::uri_to_pathbuf;
@ -295,7 +297,35 @@ impl Store {
return None;
}
let context = document.get_template_tag_context(db, position)?;
// Read content from VFS instead of using salsa-tracked document
let content = if let Ok(parsed_uri) = uri.parse::<tower_lsp_server::lsp_types::Uri>() {
if let Some(absolute_path) = uri_to_pathbuf(&parsed_uri) {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy();
// Try to read from VFS first (includes unsaved changes)
match self.vfs.read_to_string(&relative_path_str) {
Ok(vfs_content) => vfs_content,
Err(_) => {
// Fallback to document content if VFS read fails
document.contents(db).to_string()
}
}
} else {
// Path not within workspace, use document content
document.contents(db).to_string()
}
} else {
// URI parsing failed, use document content
document.contents(db).to_string()
}
} else {
// URI parsing failed, use document content
document.contents(db).to_string()
};
// Use standalone analyzer instead of salsa-tracked method
let context = Self::analyze_template_context(&content, position)?;
let mut completions: Vec<CompletionItem> = tags
.iter()
@ -332,4 +362,91 @@ impl Store {
Some(CompletionResponse::Array(completions))
}
}
/// Debug method to expose VFS state (only enabled with DJLS_DEBUG)
pub fn debug_vfs_state(&self) -> serde_json::Value {
use std::collections::HashMap;
// Get memory layer contents by trying to read all known documents
let mut memory_layer = HashMap::new();
for uri_str in self.documents.keys() {
if let Ok(uri) = uri_str.parse::<tower_lsp_server::lsp_types::Uri>() {
if let Some(absolute_path) = super::utils::uri_to_pathbuf(&uri) {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy();
// Try to read from VFS - this will show us if there's content in memory layer
if let Ok(content) = self.vfs.read_to_string(&relative_path_str) {
memory_layer.insert(relative_path_str.to_string(), content);
}
}
}
}
}
serde_json::json!({
"memory_layer_files": memory_layer,
"physical_root": self.root_path.display().to_string()
})
}
/// Extract a specific line from content string
fn get_line_from_content(content: &str, line_num: u32) -> Option<String> {
let index = LineIndex::new(content);
let start = index.line_start(line_num as usize)?;
let end = index
.line_start(line_num as usize + 1)
.unwrap_or(index.length());
Some(content[start as usize..end as usize].to_string())
}
/// Analyze template tag context from raw content (standalone, no salsa dependency)
fn analyze_template_context(content: &str, position: Position) -> Option<TemplateTagContext> {
let line = Self::get_line_from_content(content, position.line)?;
let char_pos: usize = position.character.try_into().ok()?;
let prefix = &line[..char_pos];
let rest_of_line = &line[char_pos..];
let rest_trimmed = rest_of_line.trim_start();
prefix.rfind("{%").map(|tag_start| {
// Check if we're immediately after {% with no space
let needs_leading_space = prefix.ends_with("{%");
let closing_brace = if rest_trimmed.starts_with("%}") {
ClosingBrace::FullClose
} else if rest_trimmed.starts_with('}') {
ClosingBrace::PartialClose
} else {
ClosingBrace::None
};
TemplateTagContext {
partial_tag: prefix[tag_start + 2..].trim().to_string(),
closing_brace,
needs_leading_space,
}
})
}
/// Debug method to expose Store state (only enabled with DJLS_DEBUG)
pub fn debug_store_state(&self) -> serde_json::Value {
use std::collections::HashMap;
let mut documents_info = HashMap::new();
for (uri, _doc) in &self.documents {
documents_info.insert(uri.clone(), serde_json::json!({
"version": self.versions.get(uri),
"tracked": true
}));
}
serde_json::json!({
"documents": documents_info,
"document_count": self.documents.len(),
"workspace_root": self.root_path.display().to_string()
})
}
}