mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-07 10:50:40 +00:00
wip
This commit is contained in:
parent
fb768a86d5
commit
20163b50f8
5 changed files with 94 additions and 94 deletions
|
@ -9,7 +9,10 @@ use std::{collections::HashMap, sync::Arc};
|
||||||
use salsa::Setter;
|
use salsa::Setter;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
db::{parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, TemplateLoaderOrder},
|
db::{
|
||||||
|
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst,
|
||||||
|
TemplateLoaderOrder,
|
||||||
|
},
|
||||||
vfs::{FileKind, VfsSnapshot},
|
vfs::{FileKind, VfsSnapshot},
|
||||||
FileId,
|
FileId,
|
||||||
};
|
};
|
||||||
|
@ -31,6 +34,7 @@ pub struct FileStore {
|
||||||
|
|
||||||
impl FileStore {
|
impl FileStore {
|
||||||
/// Construct an empty store and DB.
|
/// Construct an empty store and DB.
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
db: Database::default(),
|
db: Database::default(),
|
||||||
|
@ -118,8 +122,7 @@ impl FileStore {
|
||||||
pub fn get_template_errors(&self, id: FileId) -> Arc<[String]> {
|
pub fn get_template_errors(&self, id: FileId) -> Arc<[String]> {
|
||||||
self.files
|
self.files
|
||||||
.get(&id)
|
.get(&id)
|
||||||
.map(|sf| template_errors(&self.db, *sf))
|
.map_or_else(|| Arc::from(vec![]), |sf| template_errors(&self.db, *sf))
|
||||||
.unwrap_or_else(|| Arc::from(vec![]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,12 +120,15 @@ pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<
|
||||||
let text = file.text(db);
|
let text = file.text(db);
|
||||||
|
|
||||||
// Call the pure parsing function from djls-templates
|
// Call the pure parsing function from djls-templates
|
||||||
match djls_templates::parse_template(&text) {
|
match djls_templates::parse_template(text) {
|
||||||
Ok((ast, errors)) => {
|
Ok((ast, errors)) => {
|
||||||
// Convert errors to strings
|
// Convert errors to strings
|
||||||
let error_strings = errors.into_iter().map(|e| e.to_string()).collect();
|
let error_strings = errors.into_iter().map(|e| e.to_string()).collect();
|
||||||
Some(Arc::new(TemplateAst { ast, errors: error_strings }))
|
Some(Arc::new(TemplateAst {
|
||||||
},
|
ast,
|
||||||
|
errors: error_strings,
|
||||||
|
}))
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Even on fatal errors, return an empty AST with the error
|
// Even on fatal errors, return an empty AST with the error
|
||||||
Some(Arc::new(TemplateAst {
|
Some(Arc::new(TemplateAst {
|
||||||
|
@ -144,9 +147,7 @@ pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<
|
||||||
/// Returns an empty vector for non-template files.
|
/// Returns an empty vector for non-template files.
|
||||||
#[salsa::tracked]
|
#[salsa::tracked]
|
||||||
pub fn template_errors(db: &dyn salsa::Database, file: SourceFile) -> Arc<[String]> {
|
pub fn template_errors(db: &dyn salsa::Database, file: SourceFile) -> Arc<[String]> {
|
||||||
parse_template(db, file)
|
parse_template(db, file).map_or_else(|| Arc::from(vec![]), |ast| Arc::from(ast.errors.clone()))
|
||||||
.map(|ast| Arc::from(ast.errors.clone()))
|
|
||||||
.unwrap_or_else(|| Arc::from(vec![]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -187,7 +188,8 @@ mod tests {
|
||||||
assert!(ast1.is_some());
|
assert!(ast1.is_some());
|
||||||
|
|
||||||
// Change the content
|
// Change the content
|
||||||
let template_content2: Arc<str> = Arc::from("{% for item in items %}{{ item }}{% endfor %}");
|
let template_content2: Arc<str> =
|
||||||
|
Arc::from("{% for item in items %}{{ item }}{% endfor %}");
|
||||||
file.set_text(&mut db).to(template_content2);
|
file.set_text(&mut db).to(template_content2);
|
||||||
|
|
||||||
// Parse again - should re-execute due to changed content
|
// Parse again - should re-execute due to changed content
|
||||||
|
|
|
@ -3,9 +3,11 @@ mod db;
|
||||||
mod vfs;
|
mod vfs;
|
||||||
mod watcher;
|
mod watcher;
|
||||||
|
|
||||||
// Re-export public API
|
|
||||||
pub use bridge::FileStore;
|
pub use bridge::FileStore;
|
||||||
pub use db::{parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, TemplateLoaderOrder};
|
pub use db::{
|
||||||
|
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst,
|
||||||
|
TemplateLoaderOrder,
|
||||||
|
};
|
||||||
pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot};
|
pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot};
|
||||||
pub use watcher::{VfsWatcher, WatchConfig, WatchEvent};
|
pub use watcher::{VfsWatcher, WatchConfig, WatchEvent};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ use anyhow::{anyhow, Result};
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::fs;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
@ -18,7 +19,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{FileId, watcher::{VfsWatcher, WatchConfig, WatchEvent}};
|
use super::{
|
||||||
|
watcher::{VfsWatcher, WatchConfig, WatchEvent},
|
||||||
|
FileId,
|
||||||
|
};
|
||||||
|
|
||||||
/// Monotonic counter representing global VFS state.
|
/// Monotonic counter representing global VFS state.
|
||||||
///
|
///
|
||||||
|
@ -115,7 +119,7 @@ pub struct Vfs {
|
||||||
head: AtomicU64,
|
head: AtomicU64,
|
||||||
/// Optional file system watcher for external change detection
|
/// Optional file system watcher for external change detection
|
||||||
watcher: std::sync::Mutex<Option<VfsWatcher>>,
|
watcher: std::sync::Mutex<Option<VfsWatcher>>,
|
||||||
/// Map from filesystem path to FileId for watcher events
|
/// Map from filesystem path to [`FileId`] for watcher events
|
||||||
by_path: DashMap<Utf8PathBuf, FileId>,
|
by_path: DashMap<Utf8PathBuf, FileId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,10 +159,6 @@ impl Vfs {
|
||||||
/// (detected via hash comparison).
|
/// (detected via hash comparison).
|
||||||
///
|
///
|
||||||
/// Returns a tuple of (new global revision, whether content changed).
|
/// Returns a tuple of (new global revision, whether content changed).
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the provided `FileId` does not exist in the VFS.
|
|
||||||
pub fn set_overlay(&self, id: FileId, new_text: Arc<str>) -> Result<(Revision, bool)> {
|
pub fn set_overlay(&self, id: FileId, new_text: Arc<str>) -> Result<(Revision, bool)> {
|
||||||
let mut rec = self
|
let mut rec = self
|
||||||
.files
|
.files
|
||||||
|
@ -200,7 +200,10 @@ impl Vfs {
|
||||||
/// Returns an error if file watching is disabled in the config or fails to start.
|
/// Returns an error if file watching is disabled in the config or fails to start.
|
||||||
pub fn enable_file_watching(&self, config: WatchConfig) -> Result<()> {
|
pub fn enable_file_watching(&self, config: WatchConfig) -> Result<()> {
|
||||||
let watcher = VfsWatcher::new(config)?;
|
let watcher = VfsWatcher::new(config)?;
|
||||||
*self.watcher.lock().unwrap() = Some(watcher);
|
*self
|
||||||
|
.watcher
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| anyhow!("Failed to lock watcher mutex: {}", e))? = Some(watcher);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +214,9 @@ impl Vfs {
|
||||||
pub fn process_file_events(&self) -> usize {
|
pub fn process_file_events(&self) -> usize {
|
||||||
// Get events from the watcher
|
// Get events from the watcher
|
||||||
let events = {
|
let events = {
|
||||||
let guard = self.watcher.lock().unwrap();
|
let Ok(guard) = self.watcher.lock() else {
|
||||||
|
return 0; // Return 0 if mutex is poisoned
|
||||||
|
};
|
||||||
if let Some(watcher) = guard.as_ref() {
|
if let Some(watcher) = guard.as_ref() {
|
||||||
watcher.try_recv_events()
|
watcher.try_recv_events()
|
||||||
} else {
|
} else {
|
||||||
|
@ -225,22 +230,22 @@ impl Vfs {
|
||||||
match event {
|
match event {
|
||||||
WatchEvent::Modified(path) | WatchEvent::Created(path) => {
|
WatchEvent::Modified(path) | WatchEvent::Created(path) => {
|
||||||
if let Err(e) = self.load_from_disk(&path) {
|
if let Err(e) = self.load_from_disk(&path) {
|
||||||
eprintln!("Failed to load file from disk: {}: {}", path, e);
|
eprintln!("Failed to load file from disk: {path}: {e}");
|
||||||
} else {
|
} else {
|
||||||
updated_count += 1;
|
updated_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WatchEvent::Deleted(path) => {
|
WatchEvent::Deleted(path) => {
|
||||||
// For now, we don't remove deleted files from VFS
|
// For now, we don't remove deleted files from VFS
|
||||||
// This maintains stable FileIds for consumers
|
// This maintains stable `FileId`s for consumers
|
||||||
eprintln!("File deleted (keeping in VFS): {}", path);
|
eprintln!("File deleted (keeping in VFS): {path}");
|
||||||
}
|
}
|
||||||
WatchEvent::Renamed { from, to } => {
|
WatchEvent::Renamed { from, to } => {
|
||||||
// Handle rename by updating the path mapping
|
// Handle rename by updating the path mapping
|
||||||
if let Some(file_id) = self.by_path.remove(&from).map(|(_, id)| id) {
|
if let Some(file_id) = self.by_path.remove(&from).map(|(_, id)| id) {
|
||||||
self.by_path.insert(to.clone(), file_id);
|
self.by_path.insert(to.clone(), file_id);
|
||||||
if let Err(e) = self.load_from_disk(&to) {
|
if let Err(e) = self.load_from_disk(&to) {
|
||||||
eprintln!("Failed to load renamed file: {}: {}", to, e);
|
eprintln!("Failed to load renamed file: {to}: {e}");
|
||||||
} else {
|
} else {
|
||||||
updated_count += 1;
|
updated_count += 1;
|
||||||
}
|
}
|
||||||
|
@ -256,8 +261,6 @@ impl Vfs {
|
||||||
/// This method reads the file from the filesystem and updates the VFS entry
|
/// This method reads the file from the filesystem and updates the VFS entry
|
||||||
/// if the content has changed. It's used by the file watcher to sync external changes.
|
/// if the content has changed. It's used by the file watcher to sync external changes.
|
||||||
fn load_from_disk(&self, path: &Utf8PathBuf) -> Result<()> {
|
fn load_from_disk(&self, path: &Utf8PathBuf) -> Result<()> {
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
// Check if we have this file tracked
|
// Check if we have this file tracked
|
||||||
if let Some(file_id) = self.by_path.get(path).map(|entry| *entry.value()) {
|
if let Some(file_id) = self.by_path.get(path).map(|entry| *entry.value()) {
|
||||||
// Read content from disk
|
// Read content from disk
|
||||||
|
@ -281,7 +284,7 @@ impl Vfs {
|
||||||
|
|
||||||
/// Check if file watching is currently enabled.
|
/// Check if file watching is currently enabled.
|
||||||
pub fn is_file_watching_enabled(&self) -> bool {
|
pub fn is_file_watching_enabled(&self) -> bool {
|
||||||
self.watcher.lock().unwrap().is_some()
|
self.watcher.lock().map(|g| g.is_some()).unwrap_or(false) // Return false if mutex is poisoned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,7 @@ pub enum WatchEvent {
|
||||||
/// A file was deleted
|
/// A file was deleted
|
||||||
Deleted(Utf8PathBuf),
|
Deleted(Utf8PathBuf),
|
||||||
/// A file was renamed from one path to another
|
/// A file was renamed from one path to another
|
||||||
Renamed {
|
Renamed { from: Utf8PathBuf, to: Utf8PathBuf },
|
||||||
from: Utf8PathBuf,
|
|
||||||
to: Utf8PathBuf,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for the file watcher.
|
/// Configuration for the file watcher.
|
||||||
|
@ -51,6 +48,7 @@ pub struct WatchConfig {
|
||||||
pub exclude_patterns: Vec<String>,
|
pub exclude_patterns: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Allow for user config instead of hardcoding defaults
|
||||||
impl Default for WatchConfig {
|
impl Default for WatchConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -119,7 +117,7 @@ impl VfsWatcher {
|
||||||
// Spawn background thread for event processing
|
// Spawn background thread for event processing
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
Self::process_events(event_rx, watch_tx, config_clone);
|
Self::process_events(&event_rx, &watch_tx, &config_clone);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -134,23 +132,19 @@ impl VfsWatcher {
|
||||||
///
|
///
|
||||||
/// This is a non-blocking operation that returns immediately. If no events
|
/// This is a non-blocking operation that returns immediately. If no events
|
||||||
/// are available, it returns an empty vector.
|
/// are available, it returns an empty vector.
|
||||||
|
#[must_use]
|
||||||
pub fn try_recv_events(&self) -> Vec<WatchEvent> {
|
pub fn try_recv_events(&self) -> Vec<WatchEvent> {
|
||||||
match self.rx.try_recv() {
|
self.rx.try_recv().unwrap_or_default()
|
||||||
Ok(events) => events,
|
|
||||||
Err(_) => Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Background thread function for processing raw file system events.
|
/// Background thread function for processing raw file system events.
|
||||||
///
|
///
|
||||||
/// This function handles debouncing, filtering, and batching of events before
|
/// This function handles debouncing, filtering, and batching of events before
|
||||||
/// sending them to the main thread for VFS synchronization.
|
/// sending them to the main thread for VFS synchronization.
|
||||||
fn process_events(
|
fn process_events(
|
||||||
event_rx: mpsc::Receiver<Event>,
|
event_rx: &mpsc::Receiver<Event>,
|
||||||
watch_tx: mpsc::Sender<Vec<WatchEvent>>,
|
watch_tx: &mpsc::Sender<Vec<WatchEvent>>,
|
||||||
config: WatchConfig,
|
config: &WatchConfig,
|
||||||
) {
|
) {
|
||||||
let mut pending_events: HashMap<Utf8PathBuf, WatchEvent> = HashMap::new();
|
let mut pending_events: HashMap<Utf8PathBuf, WatchEvent> = HashMap::new();
|
||||||
let mut last_batch_time = Instant::now();
|
let mut last_batch_time = Instant::now();
|
||||||
|
@ -161,15 +155,14 @@ impl VfsWatcher {
|
||||||
match event_rx.recv_timeout(Duration::from_millis(50)) {
|
match event_rx.recv_timeout(Duration::from_millis(50)) {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
// Process the raw notify event into our WatchEvent format
|
// Process the raw notify event into our WatchEvent format
|
||||||
if let Some(watch_events) = Self::convert_notify_event(event, &config) {
|
if let Some(watch_events) = Self::convert_notify_event(event, config) {
|
||||||
for watch_event in watch_events {
|
for watch_event in watch_events {
|
||||||
if let Some(path) = Self::get_event_path(&watch_event) {
|
let path = Self::get_event_path(&watch_event);
|
||||||
// Only keep the latest event for each path
|
// Only keep the latest event for each path
|
||||||
pending_events.insert(path.clone(), watch_event);
|
pending_events.insert(path.clone(), watch_event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
// Timeout - check if we should flush pending events
|
// Timeout - check if we should flush pending events
|
||||||
}
|
}
|
||||||
|
@ -180,11 +173,9 @@ impl VfsWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should flush pending events
|
// Check if we should flush pending events
|
||||||
if !pending_events.is_empty()
|
if !pending_events.is_empty() && last_batch_time.elapsed() >= debounce_duration {
|
||||||
&& last_batch_time.elapsed() >= debounce_duration
|
|
||||||
{
|
|
||||||
let events: Vec<WatchEvent> = pending_events.values().cloned().collect();
|
let events: Vec<WatchEvent> = pending_events.values().cloned().collect();
|
||||||
if let Err(_) = watch_tx.send(events) {
|
if watch_tx.send(events).is_err() {
|
||||||
// Main thread disconnected, exit
|
// Main thread disconnected, exit
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -194,7 +185,7 @@ impl VfsWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a notify Event into our WatchEvent format.
|
/// Convert a [`notify::Event`] into our [`WatchEvent`] format.
|
||||||
fn convert_notify_event(event: Event, config: &WatchConfig) -> Option<Vec<WatchEvent>> {
|
fn convert_notify_event(event: Event, config: &WatchConfig) -> Option<Vec<WatchEvent>> {
|
||||||
let mut watch_events = Vec::new();
|
let mut watch_events = Vec::new();
|
||||||
|
|
||||||
|
@ -236,8 +227,7 @@ impl VfsWatcher {
|
||||||
|
|
||||||
// Check include patterns
|
// Check include patterns
|
||||||
for pattern in &config.include_patterns {
|
for pattern in &config.include_patterns {
|
||||||
if pattern.starts_with("*.") {
|
if let Some(extension) = pattern.strip_prefix("*.") {
|
||||||
let extension = &pattern[2..];
|
|
||||||
if path_str.ends_with(extension) {
|
if path_str.ends_with(extension) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -249,13 +239,13 @@ impl VfsWatcher {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the path from a WatchEvent.
|
/// Extract the path from a [`WatchEvent`].
|
||||||
fn get_event_path(event: &WatchEvent) -> Option<&Utf8PathBuf> {
|
fn get_event_path(event: &WatchEvent) -> &Utf8PathBuf {
|
||||||
match event {
|
match event {
|
||||||
WatchEvent::Modified(path) => Some(path),
|
WatchEvent::Modified(path) | WatchEvent::Created(path) | WatchEvent::Deleted(path) => {
|
||||||
WatchEvent::Created(path) => Some(path),
|
path
|
||||||
WatchEvent::Deleted(path) => Some(path),
|
}
|
||||||
WatchEvent::Renamed { to, .. } => Some(to),
|
WatchEvent::Renamed { to, .. } => to,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +260,6 @@ impl Drop for VfsWatcher {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_watch_config_default() {
|
fn test_watch_config_default() {
|
||||||
let config = WatchConfig::default();
|
let config = WatchConfig::default();
|
||||||
|
@ -328,3 +317,4 @@ mod tests {
|
||||||
assert_ne!(deleted, renamed);
|
assert_ne!(deleted, renamed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue