This commit is contained in:
Josh Thomas 2025-08-25 10:52:37 -05:00
parent 20163b50f8
commit 541200cbb1
6 changed files with 77 additions and 81 deletions

View file

@ -1,8 +1,7 @@
use djls_workspace::{FileId, VfsSnapshot};
use std::sync::Arc; use std::sync::Arc;
use tower_lsp_server::lsp_types::{Position, Range}; use tower_lsp_server::lsp_types::{Position, Range};
use djls_workspace::{FileId, VfsSnapshot};
/// Document metadata container - no longer a Salsa input, just plain data
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TextDocument { pub struct TextDocument {
pub uri: String, pub uri: String,
@ -33,7 +32,8 @@ impl TextDocument {
let content = self.get_content(vfs)?; let content = self.get_content(vfs)?;
let line_start = *line_index.line_starts.get(line as usize)?; let line_start = *line_index.line_starts.get(line as usize)?;
let line_end = line_index.line_starts let line_end = line_index
.line_starts
.get(line as usize + 1) .get(line as usize + 1)
.copied() .copied()
.unwrap_or(line_index.length); .unwrap_or(line_index.length);
@ -41,7 +41,12 @@ impl TextDocument {
Some(content[line_start as usize..line_end as usize].to_string()) Some(content[line_start as usize..line_end as usize].to_string())
} }
pub fn get_text_range(&self, vfs: &VfsSnapshot, line_index: &LineIndex, range: Range) -> Option<String> { pub fn get_text_range(
&self,
vfs: &VfsSnapshot,
line_index: &LineIndex,
range: Range,
) -> Option<String> {
let content = self.get_content(vfs)?; let content = self.get_content(vfs)?;
let start_offset = line_index.offset(range.start)? as usize; let start_offset = line_index.offset(range.start)? as usize;
@ -50,7 +55,12 @@ impl TextDocument {
Some(content[start_offset..end_offset].to_string()) Some(content[start_offset..end_offset].to_string())
} }
pub fn get_template_tag_context(&self, vfs: &VfsSnapshot, line_index: &LineIndex, position: Position) -> Option<TemplateTagContext> { pub fn get_template_tag_context(
&self,
vfs: &VfsSnapshot,
line_index: &LineIndex,
position: Position,
) -> Option<TemplateTagContext> {
let content = self.get_content(vfs)?; let content = self.get_content(vfs)?;
let start = line_index.line_starts.get(position.line as usize)?; let start = line_index.line_starts.get(position.line as usize)?;
@ -136,7 +146,9 @@ impl LineIndex {
} }
// Find the line text // Find the line text
let next_line_start = self.line_starts.get(position.line as usize + 1) let next_line_start = self
.line_starts
.get(position.line as usize + 1)
.copied() .copied()
.unwrap_or(self.length); .unwrap_or(self.length);
@ -217,4 +229,3 @@ pub struct TemplateTagContext {
pub closing_brace: ClosingBrace, pub closing_brace: ClosingBrace,
pub needs_leading_space: bool, pub needs_leading_space: bool,
} }

View file

@ -110,7 +110,8 @@ impl Store {
.ok_or_else(|| anyhow!("Line index not found for: {}", uri_str))?; .ok_or_else(|| anyhow!("Line index not found for: {}", uri_str))?;
// Apply text changes using the new function // Apply text changes using the new function
let new_content = apply_text_changes(&current_content, &params.content_changes, line_index)?; let new_content =
apply_text_changes(&current_content, &params.content_changes, line_index)?;
// Update TextDocument version // Update TextDocument version
if let Some(document) = self.documents.get_mut(&uri_str) { if let Some(document) = self.documents.get_mut(&uri_str) {
@ -317,11 +318,11 @@ fn apply_text_changes(
(Some(range_a), Some(range_b)) => { (Some(range_a), Some(range_b)) => {
// Primary sort: by line (reverse) // Primary sort: by line (reverse)
let line_cmp = range_b.start.line.cmp(&range_a.start.line); let line_cmp = range_b.start.line.cmp(&range_a.start.line);
if line_cmp != std::cmp::Ordering::Equal { if line_cmp == std::cmp::Ordering::Equal {
line_cmp
} else {
// Secondary sort: by character (reverse) // Secondary sort: by character (reverse)
range_b.start.character.cmp(&range_a.start.character) range_b.start.character.cmp(&range_a.start.character)
} else {
line_cmp
} }
} }
_ => std::cmp::Ordering::Equal, _ => std::cmp::Ordering::Equal,
@ -333,14 +334,20 @@ fn apply_text_changes(
for change in &sorted_changes { for change in &sorted_changes {
if let Some(range) = change.range { if let Some(range) = change.range {
// Convert UTF-16 positions to UTF-8 offsets // Convert UTF-16 positions to UTF-8 offsets
let start_offset = line_index.offset_utf16(range.start, &result) let start_offset = line_index
.offset_utf16(range.start, &result)
.ok_or_else(|| anyhow!("Invalid start position: {:?}", range.start))?; .ok_or_else(|| anyhow!("Invalid start position: {:?}", range.start))?;
let end_offset = line_index.offset_utf16(range.end, &result) let end_offset = line_index
.offset_utf16(range.end, &result)
.ok_or_else(|| anyhow!("Invalid end position: {:?}", range.end))?; .ok_or_else(|| anyhow!("Invalid end position: {:?}", range.end))?;
if start_offset as usize > result.len() || end_offset as usize > result.len() { if start_offset as usize > result.len() || end_offset as usize > result.len() {
return Err(anyhow!("Offset out of bounds: start={}, end={}, len={}", return Err(anyhow!(
start_offset, end_offset, result.len())); "Offset out of bounds: start={}, end={}, len={}",
start_offset,
end_offset,
result.len()
));
} }
// Apply the change // Apply the change
@ -379,7 +386,7 @@ mod tests {
let changes = vec![TextDocumentContentChangeEvent { let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range::new(Position::new(0, 5), Position::new(0, 6))), range: Some(Range::new(Position::new(0, 5), Position::new(0, 6))),
range_length: None, range_length: None,
text: "".to_string(), text: String::new(),
}]; }];
let result = apply_text_changes(content, &changes, &line_index).unwrap(); let result = apply_text_changes(content, &changes, &line_index).unwrap();

View file

@ -9,10 +9,7 @@ use std::{collections::HashMap, sync::Arc};
use salsa::Setter; use salsa::Setter;
use super::{ use super::{
db::{ db::{parse_template, template_errors, Database, SourceFile, TemplateAst, TemplateLoaderOrder},
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst,
TemplateLoaderOrder,
},
vfs::{FileKind, VfsSnapshot}, vfs::{FileKind, VfsSnapshot},
FileId, FileId,
}; };
@ -69,16 +66,12 @@ impl FileStore {
pub fn apply_vfs_snapshot(&mut self, snap: &VfsSnapshot) { pub fn apply_vfs_snapshot(&mut self, snap: &VfsSnapshot) {
for (id, rec) in &snap.files { for (id, rec) in &snap.files {
let new_text = snap.get_text(*id).unwrap_or_else(|| Arc::<str>::from("")); let new_text = snap.get_text(*id).unwrap_or_else(|| Arc::<str>::from(""));
let new_kind = match rec.meta.kind { let new_kind = rec.meta.kind;
FileKind::Python => FileKindMini::Python,
FileKind::Template => FileKindMini::Template,
FileKind::Other => FileKindMini::Other,
};
if let Some(sf) = self.files.get(id) { if let Some(sf) = self.files.get(id) {
// Update if changed — avoid touching Salsa when not needed // Update if changed — avoid touching Salsa when not needed
if sf.kind(&self.db) != new_kind { if sf.kind(&self.db) != new_kind {
sf.set_kind(&mut self.db).to(new_kind.clone()); sf.set_kind(&mut self.db).to(new_kind);
} }
if sf.text(&self.db).as_ref() != &*new_text { if sf.text(&self.db).as_ref() != &*new_text {
sf.set_text(&mut self.db).to(new_text.clone()); sf.set_text(&mut self.db).to(new_text.clone());
@ -100,7 +93,7 @@ impl FileStore {
/// Get the file kind classification by its [`FileId`]. /// Get the file kind classification by its [`FileId`].
/// ///
/// Returns `None` if the file is not tracked in the [`FileStore`]. /// Returns `None` if the file is not tracked in the [`FileStore`].
pub fn file_kind(&self, id: FileId) -> Option<FileKindMini> { pub fn file_kind(&self, id: FileId) -> Option<FileKind> {
self.files.get(&id).map(|sf| sf.kind(&self.db)) self.files.get(&id).map(|sf| sf.kind(&self.db))
} }

View file

@ -7,6 +7,8 @@ use std::sync::Arc;
#[cfg(test)] #[cfg(test)]
use std::sync::Mutex; use std::sync::Mutex;
use crate::vfs::FileKind;
/// Salsa database root for workspace /// Salsa database root for workspace
/// ///
/// The [`Database`] provides default storage and, in tests, captures Salsa events for /// The [`Database`] provides default storage and, in tests, captures Salsa events for
@ -49,21 +51,6 @@ impl Default for Database {
#[salsa::db] #[salsa::db]
impl salsa::Database for Database {} impl salsa::Database for Database {}
/// Minimal classification for analysis routing.
///
/// [`FileKindMini`] provides a lightweight categorization of files to determine which
/// analysis pipelines should process them. This is the Salsa-side representation
/// of file types, mapped from the VFS layer's `vfs::FileKind`.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum FileKindMini {
/// Python source file (.py)
Python,
/// Django template file (.html, .jinja, etc.)
Template,
/// Other file types not requiring specialized analysis
Other,
}
/// Represents a single file's classification and current content. /// Represents a single file's classification and current content.
/// ///
/// [`SourceFile`] is a Salsa input entity that tracks both the file's type (for routing /// [`SourceFile`] is a Salsa input entity that tracks both the file's type (for routing
@ -72,7 +59,7 @@ pub enum FileKindMini {
#[salsa::input] #[salsa::input]
pub struct SourceFile { pub struct SourceFile {
/// The file's classification for analysis routing /// The file's classification for analysis routing
pub kind: FileKindMini, pub kind: FileKind,
/// The current text content of the file /// The current text content of the file
#[returns(ref)] #[returns(ref)]
pub text: Arc<str>, pub text: Arc<str>,
@ -113,7 +100,7 @@ pub struct TemplateAst {
#[salsa::tracked] #[salsa::tracked]
pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<TemplateAst>> { pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<TemplateAst>> {
// Only parse template files // Only parse template files
if file.kind(db) != FileKindMini::Template { if file.kind(db) != FileKind::Template {
return None; return None;
} }
@ -161,7 +148,7 @@ mod tests {
// Create a template file // Create a template file
let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }}{% endif %}"); let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }}{% endif %}");
let file = SourceFile::new(&db, FileKindMini::Template, template_content.clone()); let file = SourceFile::new(&db, FileKind::Template, template_content.clone());
// First parse - should execute the parsing // First parse - should execute the parsing
let ast1 = parse_template(&db, file); let ast1 = parse_template(&db, file);
@ -181,7 +168,7 @@ mod tests {
// Create a template file // Create a template file
let template_content1: Arc<str> = Arc::from("{% if user %}Hello{% endif %}"); let template_content1: Arc<str> = Arc::from("{% if user %}Hello{% endif %}");
let file = SourceFile::new(&db, FileKindMini::Template, template_content1); let file = SourceFile::new(&db, FileKind::Template, template_content1);
// First parse // First parse
let ast1 = parse_template(&db, file); let ast1 = parse_template(&db, file);
@ -206,7 +193,7 @@ mod tests {
// Create a Python file // Create a Python file
let python_content: Arc<str> = Arc::from("def hello():\n print('Hello')"); let python_content: Arc<str> = Arc::from("def hello():\n print('Hello')");
let file = SourceFile::new(&db, FileKindMini::Python, python_content); let file = SourceFile::new(&db, FileKind::Python, python_content);
// Should return None for non-template files // Should return None for non-template files
let ast = parse_template(&db, file); let ast = parse_template(&db, file);
@ -223,7 +210,7 @@ mod tests {
// Create a template with an error (unclosed tag) // Create a template with an error (unclosed tag)
let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }"); let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }");
let file = SourceFile::new(&db, FileKindMini::Template, template_content); let file = SourceFile::new(&db, FileKind::Template, template_content);
// Get errors // Get errors
let errors1 = template_errors(&db, file); let errors1 = template_errors(&db, file);

View file

@ -5,8 +5,7 @@ mod watcher;
pub use bridge::FileStore; pub use bridge::FileStore;
pub use db::{ pub use db::{
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, parse_template, template_errors, Database, SourceFile, TemplateAst, TemplateLoaderOrder,
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};

View file

@ -317,4 +317,3 @@ mod tests {
assert_ne!(deleted, renamed); assert_ne!(deleted, renamed);
} }
} }