This commit is contained in:
Josh Thomas 2025-08-22 10:37:40 -05:00
parent d3134f2f22
commit 66cc577569
4 changed files with 161 additions and 11 deletions

View file

@ -10,6 +10,7 @@ use tower_lsp_server::lsp_types::DidChangeConfigurationParams;
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::InitializeParams;
use tower_lsp_server::lsp_types::InitializeResult;
use tower_lsp_server::lsp_types::InitializedParams;
@ -243,6 +244,17 @@ impl LanguageServer for DjangoLanguageServer {
.await;
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
tracing::info!("Saved document: {:?}", params.text_document.uri);
self.with_session_mut(|session| {
if let Err(e) = session.documents_mut().handle_did_save(&params) {
tracing::error!("Failed to handle did_save: {}", e);
}
})
.await;
}
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
Ok(self
.with_session(|session| {

View file

@ -295,7 +295,7 @@ mod tests {
let test_file = temp_dir.path().join("test.html");
fs::write(&test_file, "physical content").unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
let content = vfs.read_to_string("test.html").unwrap();
assert_eq!(content, "physical content");
@ -304,7 +304,7 @@ mod tests {
#[test]
fn test_write_string_and_read_to_string() {
let temp_dir = TempDir::new().unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
vfs.write_string("test.html", "memory content").unwrap();
let content = vfs.read_to_string("test.html").unwrap();
@ -318,7 +318,7 @@ mod tests {
let test_file = temp_dir.path().join("test.html");
fs::write(&test_file, "physical content").unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
// First read should get physical content
assert_eq!(vfs.read_to_string("test.html").unwrap(), "physical content");
@ -340,7 +340,7 @@ mod tests {
let test_file = temp_dir.path().join("test.html");
fs::write(&test_file, "physical content").unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
// Write to memory
vfs.write_string("test.html", "memory content").unwrap();
@ -359,7 +359,7 @@ mod tests {
let test_file = temp_dir.path().join("physical.html");
fs::write(&test_file, "content").unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
// Physical file exists
assert!(vfs.exists("physical.html").unwrap());
@ -375,7 +375,7 @@ mod tests {
#[test]
fn test_discard_changes_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let vfs = FileSystem::new(temp_dir.path()).unwrap();
let vfs = FileSystem::new(temp_dir.path());
// Should not error when clearing non-existent memory file
vfs.discard_changes("nonexistent.html").unwrap();
@ -387,7 +387,7 @@ mod tests {
let test_file = temp_dir.path().join("trait_test.html");
fs::write(&test_file, "trait physical content").unwrap();
let filesystem = FileSystem::new(temp_dir.path()).unwrap();
let filesystem = FileSystem::new(temp_dir.path());
// Test our trait implementation directly instead of through VfsPath
// VfsPath would create absolute paths which our security validation rejects
@ -428,7 +428,7 @@ mod tests {
fs::write(test_dir.join("physical1.txt"), "content").unwrap();
fs::write(test_dir.join("physical2.txt"), "content").unwrap();
let filesystem = FileSystem::new(temp_dir.path()).unwrap();
let filesystem = FileSystem::new(temp_dir.path());
// Create memory layer files using the trait methods
use vfs::FileSystem as VfsFileSystemTrait;

View file

@ -18,6 +18,7 @@
//! ### LSP Integration
//!
//! The workspace module is designed around LSP lifecycle events:
//!
//! - `textDocument/didOpen`: Files are tracked but not immediately loaded into memory
//! - `textDocument/didChange`: Changes are stored in the memory layer
//! - `textDocument/didSave`: Memory layer changes can be discarded (editor handles disk writes)
@ -34,10 +35,9 @@
//! language server operations.
mod document;
mod fs;
mod store;
mod utils;
mod fs;
pub use store::Store;
pub use utils::get_project_path;
pub use fs::FileSystem;

View file

@ -12,6 +12,8 @@ use tower_lsp_server::lsp_types::CompletionResponse;
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::TextDocumentContentChangeEvent;
use tower_lsp_server::lsp_types::Documentation;
use tower_lsp_server::lsp_types::InsertTextFormat;
use tower_lsp_server::lsp_types::MarkupContent;
@ -20,6 +22,7 @@ use tower_lsp_server::lsp_types::Position;
use super::document::ClosingBrace;
use super::document::LanguageId;
use super::document::LineIndex;
use super::document::TextDocument;
use super::utils::uri_to_pathbuf;
@ -34,7 +37,7 @@ pub struct Store {
impl Store {
pub fn new<P: AsRef<std::path::Path>>(root_path: P) -> anyhow::Result<Self> {
let root_path = root_path.as_ref().to_path_buf();
let vfs = FileSystem::new(&root_path)?;
let vfs = FileSystem::new(&root_path);
Ok(Store {
documents: HashMap::new(),
@ -63,6 +66,22 @@ impl Store {
let uri = params.text_document.uri.to_string();
let version = params.text_document.version;
let content = &params.text_document.text;
// Convert URI to relative path for VFS
if let Some(absolute_path) = uri_to_pathbuf(&params.text_document.uri) {
// Make path relative to workspace root
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
// Write content to FileSystem (memory layer for opened file)
if let Err(e) = self.vfs.write_string(
&relative_path.to_string_lossy(),
content
) {
eprintln!("Warning: Failed to write file to VFS: {}", e);
// Continue with normal processing despite VFS error
}
}
}
let document = TextDocument::from_did_open_params(db, params);
@ -84,6 +103,50 @@ impl Store {
let uri = params.text_document.uri.as_str().to_string();
let version = params.text_document.version;
// Convert URI to relative path for VFS
if let Some(absolute_path) = uri_to_pathbuf(&params.text_document.uri) {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy();
// Read current content from VFS
let current_content = match self.vfs.read_to_string(&relative_path_str) {
Ok(content) => content,
Err(e) => {
eprintln!("Warning: Failed to read from VFS, falling back to TextDocument: {}", e);
// Fallback to existing TextDocument approach
let document = self
.get_document(&uri)
.ok_or_else(|| anyhow!("Document not found: {}", uri))?;
let new_document = document.with_changes(db, &params.content_changes, version);
self.documents.insert(uri.clone(), new_document);
self.versions.insert(uri, version);
return Ok(());
}
};
// Apply text changes to VFS content
let updated_content = self.apply_changes_to_content(current_content, &params.content_changes)?;
// Write updated content back to VFS
if let Err(e) = self.vfs.write_string(&relative_path_str, &updated_content) {
eprintln!("Warning: Failed to write to VFS: {}", e);
}
// Create new TextDocument with updated content for backward compatibility
let index = LineIndex::new(&updated_content);
let language_id = self.get_document(&uri)
.map(|doc| doc.language_id(db))
.unwrap_or(LanguageId::HtmlDjango);
let new_document = TextDocument::new(db, uri.clone(), updated_content.clone(), index, version, language_id);
self.documents.insert(uri.clone(), new_document);
self.versions.insert(uri, version);
return Ok(());
}
}
// Fallback to original implementation if path conversion fails
let document = self
.get_document(&uri)
.ok_or_else(|| anyhow!("Document not found: {}", uri))?;
@ -97,9 +160,84 @@ impl Store {
}
pub fn handle_did_close(&mut self, params: &DidCloseTextDocumentParams) {
// Only process files within the workspace for VFS cleanup
if self.is_workspace_file(&params.text_document.uri) {
// Convert URI to relative path for VFS
if let Some(absolute_path) = uri_to_pathbuf(&params.text_document.uri) {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy();
// Discard any unsaved changes in VFS (clean up memory layer)
if let Err(e) = self.vfs.discard_changes(&relative_path_str) {
eprintln!("Warning: Failed to discard VFS changes on close: {}", e);
// Continue with document removal despite VFS error
}
}
}
}
// Remove document from Store tracking (always do this regardless of VFS status)
self.remove_document(params.text_document.uri.as_str());
}
pub fn handle_did_save(&mut self, params: &DidSaveTextDocumentParams) -> Result<()> {
// Only process files within the workspace
if !self.is_workspace_file(&params.text_document.uri) {
// Return Ok to avoid errors for files outside workspace
return Ok(());
}
// Convert URI to relative path for VFS
if let Some(absolute_path) = uri_to_pathbuf(&params.text_document.uri) {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy();
// Discard changes in VFS (clear memory layer so reads return disk content)
if let Err(e) = self.vfs.discard_changes(&relative_path_str) {
eprintln!("Warning: Failed to discard VFS changes on save: {}", e);
// Continue normally - this is not a critical error
}
}
}
Ok(())
}
/// Apply text changes to content (similar to TextDocument::with_changes but for strings)
fn apply_changes_to_content(
&self,
mut content: String,
changes: &[TextDocumentContentChangeEvent],
) -> Result<String> {
for change in changes {
if let Some(range) = change.range {
// Incremental change with range
let index = LineIndex::new(&content);
if let (Some(start_offset), Some(end_offset)) = (
index.offset(range.start).map(|o| o as usize),
index.offset(range.end).map(|o| o as usize),
) {
let mut updated_content = String::with_capacity(
content.len() - (end_offset - start_offset) + change.text.len(),
);
updated_content.push_str(&content[..start_offset]);
updated_content.push_str(&change.text);
updated_content.push_str(&content[end_offset..]);
content = updated_content;
} else {
return Err(anyhow!("Invalid range in text change"));
}
} else {
// Full document replacement
content.clone_from(&change.text);
}
}
Ok(content)
}
fn add_document(&mut self, document: TextDocument, uri: String) {
self.documents.insert(uri, document);
}