mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-05 01:40:51 +00:00
wip
This commit is contained in:
parent
d3134f2f22
commit
66cc577569
4 changed files with 161 additions and 11 deletions
|
@ -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(¶ms) {
|
||||
tracing::error!("Failed to handle did_save: {}", e);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
|
||||
Ok(self
|
||||
.with_session(|session| {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = ¶ms.text_document.text;
|
||||
|
||||
// Convert URI to relative path for VFS
|
||||
if let Some(absolute_path) = uri_to_pathbuf(¶ms.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(¶ms.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, ¶ms.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, ¶ms.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(¶ms.text_document.uri) {
|
||||
// Convert URI to relative path for VFS
|
||||
if let Some(absolute_path) = uri_to_pathbuf(¶ms.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(¶ms.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(¶ms.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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue