mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-13 22:06:19 +00:00
Fix overlay bug: Salsa wasn't re-reading from buffers when files were opened
The core issue was that when a file was opened in the LSP, if it had already been read from disk, Salsa would return cached content instead of reading from the overlay system. This happened because opening a file didn't bump its revision, so Salsa had no reason to invalidate its cache. Key changes: - Created Buffers abstraction to encapsulate shared buffer storage - Fixed Session::open_document() to bump revision when file already exists - Added comprehensive integration tests to verify overlay behavior - Refactored WorkspaceFileSystem to use Buffers instead of raw DashMap This ensures that overlays always take precedence over disk content, fixing the issue where LSP edits weren't being reflected in template parsing.
This commit is contained in:
parent
21403df0ba
commit
2dd779bcda
9 changed files with 650 additions and 89 deletions
68
crates/djls-workspace/src/buffers.rs
Normal file
68
crates/djls-workspace/src/buffers.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
//! Shared buffer storage for open documents
|
||||
//!
|
||||
//! This module provides the `Buffers` type which represents the in-memory
|
||||
//! content of open files. These buffers are shared between the Session
|
||||
//! (which manages document lifecycle) and the WorkspaceFileSystem (which
|
||||
//! reads from them).
|
||||
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
use crate::document::TextDocument;
|
||||
|
||||
/// Shared buffer storage between Session and FileSystem
|
||||
///
|
||||
/// Buffers represent the in-memory content of open files that takes
|
||||
/// precedence over disk content when reading through the FileSystem.
|
||||
/// This is the key abstraction that makes the sharing between Session
|
||||
/// and WorkspaceFileSystem explicit and type-safe.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Buffers {
|
||||
inner: Arc<DashMap<Url, TextDocument>>,
|
||||
}
|
||||
|
||||
impl Buffers {
|
||||
/// Create a new empty buffer storage
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a document in the buffers
|
||||
pub fn open(&self, url: Url, document: TextDocument) {
|
||||
self.inner.insert(url, document);
|
||||
}
|
||||
|
||||
/// Update an open document
|
||||
pub fn update(&self, url: Url, document: TextDocument) {
|
||||
self.inner.insert(url, document);
|
||||
}
|
||||
|
||||
/// Close a document and return it if it was open
|
||||
pub fn close(&self, url: &Url) -> Option<TextDocument> {
|
||||
self.inner.remove(url).map(|(_, doc)| doc)
|
||||
}
|
||||
|
||||
/// Get a document if it's open
|
||||
pub fn get(&self, url: &Url) -> Option<TextDocument> {
|
||||
self.inner.get(url).map(|entry| entry.clone())
|
||||
}
|
||||
|
||||
/// Check if a document is open
|
||||
pub fn contains(&self, url: &Url) -> bool {
|
||||
self.inner.contains_key(url)
|
||||
}
|
||||
|
||||
/// Iterate over all open buffers (for debugging)
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Url, TextDocument)> + '_ {
|
||||
self.inner.iter().map(|entry| (entry.key().clone(), entry.value().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Buffers {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -398,6 +398,7 @@ pub fn template_errors(db: &dyn Db, file: SourceFile) -> Arc<[String]> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::buffers::Buffers;
|
||||
use crate::document::TextDocument;
|
||||
use crate::fs::WorkspaceFileSystem;
|
||||
use crate::language::LanguageId;
|
||||
|
@ -464,11 +465,11 @@ mod tests {
|
|||
);
|
||||
|
||||
// Create overlay storage
|
||||
let overlays = Arc::new(DashMap::new());
|
||||
let buffers = Buffers::new();
|
||||
|
||||
// Create WorkspaceFileSystem that checks overlays first
|
||||
let file_system = Arc::new(WorkspaceFileSystem::new(
|
||||
overlays.clone(),
|
||||
buffers.clone(),
|
||||
Arc::new(memory_fs),
|
||||
));
|
||||
|
||||
|
@ -490,7 +491,7 @@ mod tests {
|
|||
2,
|
||||
LanguageId::Other,
|
||||
);
|
||||
overlays.insert(url, updated_document);
|
||||
buffers.open(url, updated_document);
|
||||
|
||||
// Bump the file revision to trigger re-parse
|
||||
file.set_revision(&mut db).to(1);
|
||||
|
@ -520,11 +521,11 @@ mod tests {
|
|||
);
|
||||
|
||||
// Create overlay storage
|
||||
let overlays = Arc::new(DashMap::new());
|
||||
let buffers = Buffers::new();
|
||||
|
||||
// Create WorkspaceFileSystem
|
||||
let file_system = Arc::new(WorkspaceFileSystem::new(
|
||||
overlays.clone(),
|
||||
buffers.clone(),
|
||||
Arc::new(memory_fs),
|
||||
));
|
||||
|
||||
|
@ -549,7 +550,7 @@ mod tests {
|
|||
2,
|
||||
LanguageId::Other,
|
||||
);
|
||||
overlays.insert(url, updated_document);
|
||||
buffers.open(url, updated_document);
|
||||
|
||||
// Bump revision to trigger invalidation
|
||||
file.set_revision(&mut db).to(1);
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
//! This module provides the `FileSystem` trait that abstracts file I/O operations.
|
||||
//! This allows the LSP to work with both real files and in-memory overlays.
|
||||
|
||||
use dashmap::DashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
use crate::document::TextDocument;
|
||||
use crate::buffers::Buffers;
|
||||
|
||||
/// Trait for file system operations
|
||||
///
|
||||
|
@ -66,43 +65,43 @@ impl FileSystem for OsFileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/// LSP file system that intercepts reads for overlay files
|
||||
/// LSP file system that intercepts reads for buffered files
|
||||
///
|
||||
/// This implements Ruff's two-layer architecture where Layer 1 (LSP overlays)
|
||||
/// This implements Ruff's two-layer architecture where Layer 1 (open buffers)
|
||||
/// takes precedence over Layer 2 (Salsa database). When a file is read,
|
||||
/// this system first checks for an overlay (in-memory changes) and returns
|
||||
/// that content. If no overlay exists, it falls back to reading from disk.
|
||||
/// this system first checks for a buffer (in-memory content) and returns
|
||||
/// that content. If no buffer exists, it falls back to reading from disk.
|
||||
pub struct WorkspaceFileSystem {
|
||||
/// In-memory overlays that take precedence over disk files
|
||||
/// Maps URL to `TextDocument` containing current content
|
||||
buffers: Arc<DashMap<Url, TextDocument>>,
|
||||
/// In-memory buffers that take precedence over disk files
|
||||
buffers: Buffers,
|
||||
/// Fallback file system for disk operations
|
||||
disk: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl WorkspaceFileSystem {
|
||||
/// Create a new [`LspFileSystem`] with the given overlay storage and fallback
|
||||
pub fn new(buffers: Arc<DashMap<Url, TextDocument>>, disk: Arc<dyn FileSystem>) -> Self {
|
||||
/// Create a new [`WorkspaceFileSystem`] with the given buffer storage and fallback
|
||||
pub fn new(buffers: Buffers, disk: Arc<dyn FileSystem>) -> Self {
|
||||
Self { buffers, disk }
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for WorkspaceFileSystem {
|
||||
fn read_to_string(&self, path: &Path) -> io::Result<String> {
|
||||
if let Some(document) = path_to_url(path).and_then(|url| self.buffers.get(&url)) {
|
||||
Ok(document.content().to_string())
|
||||
} else {
|
||||
self.disk.read_to_string(path)
|
||||
if let Some(url) = path_to_url(path) {
|
||||
if let Some(document) = self.buffers.get(&url) {
|
||||
return Ok(document.content().to_string());
|
||||
}
|
||||
}
|
||||
self.disk.read_to_string(path)
|
||||
}
|
||||
|
||||
fn exists(&self, path: &Path) -> bool {
|
||||
path_to_url(path).is_some_and(|url| self.buffers.contains_key(&url))
|
||||
path_to_url(path).is_some_and(|url| self.buffers.contains(&url))
|
||||
|| self.disk.exists(path)
|
||||
}
|
||||
|
||||
fn is_file(&self, path: &Path) -> bool {
|
||||
path_to_url(path).is_some_and(|url| self.buffers.contains_key(&url))
|
||||
path_to_url(path).is_some_and(|url| self.buffers.contains(&url))
|
||||
|| self.disk.is_file(path)
|
||||
}
|
||||
|
||||
|
@ -128,22 +127,24 @@ impl FileSystem for WorkspaceFileSystem {
|
|||
/// This is a simplified conversion - in a full implementation,
|
||||
/// you might want more robust path-to-URL conversion
|
||||
fn path_to_url(path: &Path) -> Option<Url> {
|
||||
if let Ok(absolute_path) = std::fs::canonicalize(path) {
|
||||
return Url::from_file_path(absolute_path).ok();
|
||||
}
|
||||
|
||||
// For test scenarios where the file doesn't exist on disk,
|
||||
// try to create URL from the path directly if it's absolute
|
||||
// For absolute paths, use them directly without canonicalization
|
||||
// This ensures consistency with how URLs are created when storing overlays
|
||||
if path.is_absolute() {
|
||||
return Url::from_file_path(path).ok();
|
||||
}
|
||||
|
||||
// Only try to canonicalize for relative paths
|
||||
if let Ok(absolute_path) = std::fs::canonicalize(path) {
|
||||
return Url::from_file_path(absolute_path).ok();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::buffers::Buffers;
|
||||
use crate::document::TextDocument;
|
||||
use crate::language::LanguageId;
|
||||
|
||||
|
@ -207,22 +208,22 @@ mod tests {
|
|||
"original content".to_string(),
|
||||
);
|
||||
|
||||
// Create overlay storage
|
||||
let overlays = Arc::new(DashMap::new());
|
||||
// Create buffer storage
|
||||
let buffers = Buffers::new();
|
||||
|
||||
// Create LspFileSystem with memory fallback
|
||||
let lsp_fs = WorkspaceFileSystem::new(overlays.clone(), Arc::new(memory_fs));
|
||||
let lsp_fs = WorkspaceFileSystem::new(buffers.clone(), Arc::new(memory_fs));
|
||||
|
||||
// Before adding overlay, should read from fallback
|
||||
// Before adding buffer, should read from fallback
|
||||
let path = std::path::Path::new("/test/file.py");
|
||||
assert_eq!(lsp_fs.read_to_string(path).unwrap(), "original content");
|
||||
|
||||
// Add overlay - this simulates having an open document with changes
|
||||
// Add buffer - this simulates having an open document with changes
|
||||
let url = Url::from_file_path("/test/file.py").unwrap();
|
||||
let document = TextDocument::new("overlay content".to_string(), 1, LanguageId::Python);
|
||||
overlays.insert(url, document);
|
||||
buffers.open(url, document);
|
||||
|
||||
// Now should read from overlay
|
||||
// Now should read from buffer
|
||||
assert_eq!(lsp_fs.read_to_string(path).unwrap(), "overlay content");
|
||||
}
|
||||
|
||||
|
@ -235,13 +236,13 @@ mod tests {
|
|||
"disk content".to_string(),
|
||||
);
|
||||
|
||||
// Create empty overlay storage
|
||||
let overlays = Arc::new(DashMap::new());
|
||||
// Create empty buffer storage
|
||||
let buffers = Buffers::new();
|
||||
|
||||
// Create LspFileSystem
|
||||
let lsp_fs = WorkspaceFileSystem::new(overlays, Arc::new(memory_fs));
|
||||
let lsp_fs = WorkspaceFileSystem::new(buffers, Arc::new(memory_fs));
|
||||
|
||||
// Should fall back to disk when no overlay exists
|
||||
// Should fall back to disk when no buffer exists
|
||||
let path = std::path::Path::new("/test/file.py");
|
||||
assert_eq!(lsp_fs.read_to_string(path).unwrap(), "disk content");
|
||||
}
|
||||
|
@ -256,8 +257,8 @@ mod tests {
|
|||
);
|
||||
|
||||
// Create LspFileSystem
|
||||
let overlays = Arc::new(DashMap::new());
|
||||
let lsp_fs = WorkspaceFileSystem::new(overlays, Arc::new(memory_fs));
|
||||
let buffers = Buffers::new();
|
||||
let lsp_fs = WorkspaceFileSystem::new(buffers, Arc::new(memory_fs));
|
||||
|
||||
let path = std::path::Path::new("/test/file.py");
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
mod buffers;
|
||||
pub mod db;
|
||||
mod document;
|
||||
mod fs;
|
||||
mod language;
|
||||
mod template;
|
||||
|
||||
pub use buffers::Buffers;
|
||||
pub use db::Database;
|
||||
pub use document::TextDocument;
|
||||
pub use fs::{FileSystem, OsFileSystem, WorkspaceFileSystem};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue