mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-10 20:36:21 +00:00
Add virtual FileSystem
for workspace file management
This commit is contained in:
parent
2086f80cc0
commit
734d4495e0
7 changed files with 411 additions and 3 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -462,11 +462,13 @@ dependencies = [
|
|||
"salsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tower-lsp-server",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"vfs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -549,6 +551,18 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.1.4"
|
||||
|
@ -809,6 +823,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
|||
dependencies = [
|
||||
"bitflags 2.9.2",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1877,6 +1892,15 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vfs"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e723b9e1c02a3cf9f9d0de6a4ddb8cdc1df859078902fe0ae0589d615711ae6"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
|
|
|
@ -29,6 +29,7 @@ toml = "0.9"
|
|||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
|
||||
vfs = "0.12.2"
|
||||
which = "8.0"
|
||||
|
||||
# testing
|
||||
|
|
|
@ -23,9 +23,13 @@ tower-lsp-server = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
vfs = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
djls-dev = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -7,7 +7,7 @@ use tower_lsp_server::lsp_types::InitializeParams;
|
|||
use crate::db::ServerDatabase;
|
||||
use crate::workspace::Store;
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
pub struct Session {
|
||||
project: Option<DjangoProject>,
|
||||
documents: Store,
|
||||
|
@ -65,7 +65,7 @@ impl Session {
|
|||
Self {
|
||||
client_capabilities: params.capabilities.clone(),
|
||||
project,
|
||||
documents: Store::default(),
|
||||
documents: Store::new(project_path.as_ref().unwrap_or(&std::env::current_dir().unwrap())).unwrap(),
|
||||
settings,
|
||||
db_handle: StorageHandle::new(None),
|
||||
}
|
||||
|
|
362
crates/djls-server/src/workspace/fs.rs
Normal file
362
crates/djls-server/src/workspace/fs.rs
Normal file
|
@ -0,0 +1,362 @@
|
|||
use std::path::Path;
|
||||
use vfs::{MemoryFS, PhysicalFS, VfsPath, VfsResult, VfsMetadata, SeekAndRead, SeekAndWrite};
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// A file system for managing workspace file content with manual layer management.
|
||||
///
|
||||
/// The file system uses two separate layers:
|
||||
/// - Memory layer: for unsaved edits and temporary content
|
||||
/// - Physical layer: for disk-based files
|
||||
///
|
||||
/// When reading, the memory layer is checked first, falling back to physical layer.
|
||||
/// Write operations go to memory layer only, preserving original files on disk.
|
||||
/// Clearing memory layer allows immediate fallback to physical layer without whiteout markers.
|
||||
#[derive(Debug)]
|
||||
pub struct FileSystem {
|
||||
memory: VfsPath,
|
||||
physical: VfsPath,
|
||||
}
|
||||
|
||||
impl FileSystem {
|
||||
/// Create a new FileSystem with separate memory and physical layers
|
||||
pub fn new<P: AsRef<Path>>(root_path: P) -> VfsResult<Self> {
|
||||
let memory = VfsPath::new(MemoryFS::new());
|
||||
let physical = VfsPath::new(PhysicalFS::new(root_path.as_ref()));
|
||||
|
||||
Ok(FileSystem { memory, physical })
|
||||
}
|
||||
|
||||
/// Read content from the file system
|
||||
/// Checks memory layer first, then falls back to physical layer
|
||||
pub fn read(&self, path: &str) -> VfsResult<String> {
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.read_to_string();
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.read_to_string()
|
||||
}
|
||||
|
||||
/// Write content to memory layer only
|
||||
/// This preserves the original file on disk while allowing edits
|
||||
pub fn write_memory(&self, path: &str, content: &str) -> VfsResult<()> {
|
||||
let memory_path = self.memory.join(path)?;
|
||||
|
||||
// Ensure parent directories exist in memory layer
|
||||
let parent = memory_path.parent();
|
||||
if !parent.is_root() && !parent.exists()? {
|
||||
parent.create_dir_all()?;
|
||||
}
|
||||
|
||||
memory_path.create_file()?.write_all(content.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear memory layer content for a specific path
|
||||
/// After clearing, reads will fall back to physical layer
|
||||
/// No whiteout markers are created - direct memory layer management
|
||||
pub fn clear_memory(&self, path: &str) -> VfsResult<()> {
|
||||
let memory_path = self.memory.join(path)?;
|
||||
|
||||
// Only remove if it exists in memory layer
|
||||
if memory_path.exists()? {
|
||||
memory_path.remove_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a path exists in either layer
|
||||
/// Checks memory layer first, then physical layer
|
||||
pub fn exists(&self, path: &str) -> VfsResult<bool> {
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.exists()
|
||||
}
|
||||
|
||||
/// Get memory layer root for advanced operations
|
||||
pub fn memory_root(&self) -> VfsPath {
|
||||
self.memory.clone()
|
||||
}
|
||||
|
||||
/// Get physical layer root for advanced operations
|
||||
pub fn physical_root(&self) -> VfsPath {
|
||||
self.physical.clone()
|
||||
}
|
||||
|
||||
/// Get root for backward compatibility (returns memory root)
|
||||
pub fn root(&self) -> VfsPath {
|
||||
self.memory.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Implement vfs::FileSystem trait to make our FileSystem compatible with VfsPath
|
||||
impl vfs::FileSystem for FileSystem {
|
||||
fn read_dir(&self, path: &str) -> VfsResult<Box<dyn Iterator<Item = String> + Send>> {
|
||||
// Check memory layer first, then physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return Ok(Box::new(memory_path.read_dir()?.map(|p| p.filename())));
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
Ok(Box::new(physical_path.read_dir()?.map(|p| p.filename())))
|
||||
}
|
||||
|
||||
fn create_dir(&self, path: &str) -> VfsResult<()> {
|
||||
// Create directory in memory layer only
|
||||
let memory_path = self.memory.join(path)?;
|
||||
memory_path.create_dir()
|
||||
}
|
||||
|
||||
fn open_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndRead + Send>> {
|
||||
// Check memory layer first, then physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.open_file();
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.open_file()
|
||||
}
|
||||
|
||||
fn create_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
||||
// Create file in memory layer only
|
||||
let memory_path = self.memory.join(path)?;
|
||||
|
||||
// Ensure parent directories exist in memory layer
|
||||
let parent = memory_path.parent();
|
||||
if !parent.is_root() && !parent.exists()? {
|
||||
parent.create_dir_all()?;
|
||||
}
|
||||
|
||||
memory_path.create_file()
|
||||
}
|
||||
|
||||
fn append_file(&self, path: &str) -> VfsResult<Box<dyn SeekAndWrite + Send>> {
|
||||
// For append, we need to check if file exists and copy to memory if needed
|
||||
let memory_path = self.memory.join(path)?;
|
||||
|
||||
if !memory_path.exists()? {
|
||||
// Copy from physical to memory first if it exists
|
||||
let physical_path = self.physical.join(path)?;
|
||||
if physical_path.exists()? {
|
||||
let content = physical_path.read_to_string()?;
|
||||
self.write_memory(path, &content)?;
|
||||
}
|
||||
}
|
||||
|
||||
memory_path.append_file()
|
||||
}
|
||||
|
||||
fn metadata(&self, path: &str) -> VfsResult<VfsMetadata> {
|
||||
// Check memory layer first, then physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.metadata();
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.metadata()
|
||||
}
|
||||
|
||||
fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
|
||||
// Set on memory layer if exists, otherwise physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.set_creation_time(time);
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.set_creation_time(time)
|
||||
}
|
||||
|
||||
fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
|
||||
// Set on memory layer if exists, otherwise physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.set_modification_time(time);
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.set_modification_time(time)
|
||||
}
|
||||
|
||||
fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
|
||||
// Set on memory layer if exists, otherwise physical
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
return memory_path.set_access_time(time);
|
||||
}
|
||||
|
||||
let physical_path = self.physical.join(path)?;
|
||||
physical_path.set_access_time(time)
|
||||
}
|
||||
|
||||
fn exists(&self, path: &str) -> VfsResult<bool> {
|
||||
// Use our existing method which already handles layer checking
|
||||
self.exists(path)
|
||||
}
|
||||
|
||||
fn remove_file(&self, path: &str) -> VfsResult<()> {
|
||||
// Only remove from memory layer - this aligns with our LSP semantics
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
memory_path.remove_file()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_dir(&self, path: &str) -> VfsResult<()> {
|
||||
// Only remove from memory layer - this aligns with our LSP semantics
|
||||
let memory_path = self.memory.join(path)?;
|
||||
if memory_path.exists()? {
|
||||
memory_path.remove_dir()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_new_vfs() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
// Should be able to get roots
|
||||
let _memory_root = vfs.memory_root();
|
||||
let _physical_root = vfs.physical_root();
|
||||
let _root = vfs.root();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_physical_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
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 content = vfs.read("test.html").unwrap();
|
||||
|
||||
assert_eq!(content, "physical content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_memory_and_read() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
vfs.write_memory("test.html", "memory content").unwrap();
|
||||
let content = vfs.read("test.html").unwrap();
|
||||
|
||||
assert_eq!(content, "memory content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_layer_priority() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let test_file = temp_dir.path().join("test.html");
|
||||
fs::write(&test_file, "physical content").unwrap();
|
||||
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
// First read should get physical content
|
||||
assert_eq!(vfs.read("test.html").unwrap(), "physical content");
|
||||
|
||||
// Write to memory layer
|
||||
vfs.write_memory("test.html", "memory content").unwrap();
|
||||
|
||||
// Now read should get memory content (higher priority)
|
||||
assert_eq!(vfs.read("test.html").unwrap(), "memory content");
|
||||
|
||||
// Physical file should remain unchanged
|
||||
let physical_content = fs::read_to_string(&test_file).unwrap();
|
||||
assert_eq!(physical_content, "physical content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_memory_fallback() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let test_file = temp_dir.path().join("test.html");
|
||||
fs::write(&test_file, "physical content").unwrap();
|
||||
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
// Write to memory
|
||||
vfs.write_memory("test.html", "memory content").unwrap();
|
||||
assert_eq!(vfs.read("test.html").unwrap(), "memory content");
|
||||
|
||||
// Clear memory
|
||||
vfs.clear_memory("test.html").unwrap();
|
||||
|
||||
// Should now read from physical layer
|
||||
assert_eq!(vfs.read("test.html").unwrap(), "physical content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exists() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let test_file = temp_dir.path().join("physical.html");
|
||||
fs::write(&test_file, "content").unwrap();
|
||||
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
// Physical file exists
|
||||
assert!(vfs.exists("physical.html").unwrap());
|
||||
|
||||
// Memory file exists after writing
|
||||
vfs.write_memory("memory.html", "content").unwrap();
|
||||
assert!(vfs.exists("memory.html").unwrap());
|
||||
|
||||
// Non-existent file
|
||||
assert!(!vfs.exists("nonexistent.html").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_memory_nonexistent() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let vfs = FileSystem::new(temp_dir.path()).unwrap();
|
||||
|
||||
// Should not error when clearing non-existent memory file
|
||||
vfs.clear_memory("nonexistent.html").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vfs_trait_implementation() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
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 vfs_path = VfsPath::new(filesystem);
|
||||
|
||||
// Test that our FileSystem works as a vfs::FileSystem trait
|
||||
assert!(vfs_path.join("trait_test.html").unwrap().exists().unwrap());
|
||||
|
||||
// Test reading through trait
|
||||
let content = vfs_path.join("trait_test.html").unwrap().read_to_string().unwrap();
|
||||
assert_eq!(content, "trait physical content");
|
||||
|
||||
// Test creating file through trait
|
||||
let new_file = vfs_path.join("new_trait_file.html").unwrap();
|
||||
new_file.create_file().unwrap().write_all(b"trait memory content").unwrap();
|
||||
|
||||
// Should read from memory layer
|
||||
let memory_content = new_file.read_to_string().unwrap();
|
||||
assert_eq!(memory_content, "trait memory content");
|
||||
|
||||
// Physical file should not exist (memory layer only)
|
||||
let physical_new_file = temp_dir.path().join("new_trait_file.html");
|
||||
assert!(!physical_new_file.exists());
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
mod document;
|
||||
mod store;
|
||||
mod utils;
|
||||
mod fs;
|
||||
|
||||
pub use store::Store;
|
||||
pub use utils::get_project_path;
|
||||
pub use fs::FileSystem;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use super::fs::FileSystem;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
|
@ -20,13 +22,26 @@ use super::document::ClosingBrace;
|
|||
use super::document::LanguageId;
|
||||
use super::document::TextDocument;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
documents: HashMap<String, TextDocument>,
|
||||
versions: HashMap<String, i32>,
|
||||
vfs: FileSystem,
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(Store {
|
||||
documents: HashMap::new(),
|
||||
versions: HashMap::new(),
|
||||
vfs,
|
||||
root_path,
|
||||
})
|
||||
}
|
||||
pub fn handle_did_open(&mut self, db: &dyn Database, params: &DidOpenTextDocumentParams) {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
let version = params.text_document.version;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue