diff --git a/crates/djls-server/src/workspace/document.rs b/crates/djls-server/src/workspace/document.rs index 5a77a65..75812b8 100644 --- a/crates/djls-server/src/workspace/document.rs +++ b/crates/djls-server/src/workspace/document.rs @@ -1,8 +1,7 @@ +use djls_workspace::{FileId, VfsSnapshot}; use std::sync::Arc; 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)] pub struct TextDocument { pub uri: String, @@ -20,39 +19,50 @@ impl TextDocument { file_id, } } - + pub fn file_id(&self) -> FileId { self.file_id } - + pub fn get_content(&self, vfs: &VfsSnapshot) -> Option> { vfs.get_text(self.file_id) } - + pub fn get_line(&self, vfs: &VfsSnapshot, line_index: &LineIndex, line: u32) -> Option { let content = self.get_content(vfs)?; - + 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) .copied() .unwrap_or(line_index.length); - + 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 { + + pub fn get_text_range( + &self, + vfs: &VfsSnapshot, + line_index: &LineIndex, + range: Range, + ) -> Option { let content = self.get_content(vfs)?; - + let start_offset = line_index.offset(range.start)? as usize; let end_offset = line_index.offset(range.end)? as usize; - + Some(content[start_offset..end_offset].to_string()) } - - pub fn get_template_tag_context(&self, vfs: &VfsSnapshot, line_index: &LineIndex, position: Position) -> Option { + + pub fn get_template_tag_context( + &self, + vfs: &VfsSnapshot, + line_index: &LineIndex, + position: Position, + ) -> Option { let content = self.get_content(vfs)?; - + let start = line_index.line_starts.get(position.line as usize)?; let end = line_index .line_starts @@ -136,16 +146,18 @@ impl LineIndex { } // 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() .unwrap_or(self.length); - + let line_text = text.get(*line_start_utf8 as usize..next_line_start as usize)?; - + // Convert UTF-16 character offset to UTF-8 byte offset within the line let mut utf16_pos = 0; let mut utf8_pos = 0; - + for c in line_text.chars() { if utf16_pos >= position.character { break; @@ -153,7 +165,7 @@ impl LineIndex { utf16_pos += u32::try_from(c.len_utf16()).unwrap_or(0); utf8_pos += u32::try_from(c.len_utf8()).unwrap_or(0); } - + Some(line_start_utf8 + utf8_pos) } @@ -217,4 +229,3 @@ pub struct TemplateTagContext { pub closing_brace: ClosingBrace, pub needs_leading_space: bool, } - diff --git a/crates/djls-server/src/workspace/store.rs b/crates/djls-server/src/workspace/store.rs index 5a38d37..9096201 100644 --- a/crates/djls-server/src/workspace/store.rs +++ b/crates/djls-server/src/workspace/store.rs @@ -110,7 +110,8 @@ impl Store { .ok_or_else(|| anyhow!("Line index not found for: {}", uri_str))?; // Apply text changes using the new function - let new_content = apply_text_changes(¤t_content, ¶ms.content_changes, line_index)?; + let new_content = + apply_text_changes(¤t_content, ¶ms.content_changes, line_index)?; // Update TextDocument version if let Some(document) = self.documents.get_mut(&uri_str) { @@ -317,11 +318,11 @@ fn apply_text_changes( (Some(range_a), Some(range_b)) => { // Primary sort: by line (reverse) let line_cmp = range_b.start.line.cmp(&range_a.start.line); - if line_cmp != std::cmp::Ordering::Equal { - line_cmp - } else { + if line_cmp == std::cmp::Ordering::Equal { // Secondary sort: by character (reverse) range_b.start.character.cmp(&range_a.start.character) + } else { + line_cmp } } _ => std::cmp::Ordering::Equal, @@ -333,14 +334,20 @@ fn apply_text_changes( for change in &sorted_changes { if let Some(range) = change.range { // 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))?; - 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))?; if start_offset as usize > result.len() || end_offset as usize > result.len() { - return Err(anyhow!("Offset out of bounds: start={}, end={}, len={}", - start_offset, end_offset, result.len())); + return Err(anyhow!( + "Offset out of bounds: start={}, end={}, len={}", + start_offset, + end_offset, + result.len() + )); } // Apply the change @@ -360,7 +367,7 @@ mod tests { fn test_apply_single_character_insertion() { let content = "Hello world"; let line_index = LineIndex::new(content); - + let changes = vec![TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 6), Position::new(0, 6))), range_length: None, @@ -375,11 +382,11 @@ mod tests { fn test_apply_single_character_deletion() { let content = "Hello world"; let line_index = LineIndex::new(content); - + let changes = vec![TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 5), Position::new(0, 6))), range_length: None, - text: "".to_string(), + text: String::new(), }]; let result = apply_text_changes(content, &changes, &line_index).unwrap(); @@ -390,7 +397,7 @@ mod tests { fn test_apply_multiple_changes_in_reverse_order() { let content = "line 1\nline 2\nline 3"; let line_index = LineIndex::new(content); - + // Insert "new " at position (1, 0) and "another " at position (0, 0) let changes = vec![ TextDocumentContentChangeEvent { @@ -413,7 +420,7 @@ mod tests { fn test_apply_multiline_replacement() { let content = "line 1\nline 2\nline 3"; let line_index = LineIndex::new(content); - + let changes = vec![TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 0), Position::new(2, 6))), range_length: None, @@ -428,7 +435,7 @@ mod tests { fn test_apply_full_document_replacement() { let content = "old content"; let line_index = LineIndex::new(content); - + let changes = vec![TextDocumentContentChangeEvent { range: None, range_length: None, @@ -443,7 +450,7 @@ mod tests { fn test_utf16_line_index_basic() { let content = "hello world"; let line_index = LineIndex::new(content); - + // ASCII characters should have 1:1 UTF-8:UTF-16 mapping let pos = Position::new(0, 6); let offset = line_index.offset_utf16(pos, content).unwrap(); @@ -455,11 +462,11 @@ mod tests { fn test_utf16_line_index_with_emoji() { let content = "hello 👋 world"; let line_index = LineIndex::new(content); - + // 👋 is 2 UTF-16 code units but 4 UTF-8 bytes let pos_after_emoji = Position::new(0, 8); // UTF-16 position after "hello 👋" let offset = line_index.offset_utf16(pos_after_emoji, content).unwrap(); - + // Should point to the space before "world" assert_eq!(offset, 10); // UTF-8 byte offset assert_eq!(&content[10..11], " "); @@ -469,7 +476,7 @@ mod tests { fn test_utf16_line_index_multiline() { let content = "first line\nsecond line"; let line_index = LineIndex::new(content); - + let pos = Position::new(1, 7); // Position at 'l' in "line" on second line let offset = line_index.offset_utf16(pos, content).unwrap(); assert_eq!(offset, 18); // 11 (first line + \n) + 7 @@ -480,7 +487,7 @@ mod tests { fn test_apply_changes_with_emoji() { let content = "hello 👋 world"; let line_index = LineIndex::new(content); - + // Insert text after the space following the emoji (UTF-16 position 9) let changes = vec![TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 9), Position::new(0, 9))), @@ -496,28 +503,28 @@ mod tests { fn test_line_index_utf16_tracking() { let content = "a👋b"; let line_index = LineIndex::new(content); - + // Check UTF-16 line starts are tracked correctly assert_eq!(line_index.line_starts_utf16, vec![0]); assert_eq!(line_index.length_utf16, 4); // 'a' (1) + 👋 (2) + 'b' (1) = 4 UTF-16 units assert_eq!(line_index.length, 6); // 'a' (1) + 👋 (4) + 'b' (1) = 6 UTF-8 bytes } - #[test] + #[test] fn test_edge_case_changes_at_boundaries() { let content = "abc"; let line_index = LineIndex::new(content); - + // Insert at beginning let changes = vec![TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 0), Position::new(0, 0))), range_length: None, text: "start".to_string(), }]; - + let result = apply_text_changes(content, &changes, &line_index).unwrap(); assert_eq!(result, "startabc"); - + // Insert at end let line_index = LineIndex::new(content); let changes = vec![TextDocumentContentChangeEvent { @@ -525,7 +532,7 @@ mod tests { range_length: None, text: "end".to_string(), }]; - + let result = apply_text_changes(content, &changes, &line_index).unwrap(); assert_eq!(result, "abcend"); } diff --git a/crates/djls-workspace/src/bridge.rs b/crates/djls-workspace/src/bridge.rs index 78669ad..767afbe 100644 --- a/crates/djls-workspace/src/bridge.rs +++ b/crates/djls-workspace/src/bridge.rs @@ -9,10 +9,7 @@ use std::{collections::HashMap, sync::Arc}; use salsa::Setter; use super::{ - db::{ - parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, - TemplateLoaderOrder, - }, + db::{parse_template, template_errors, Database, SourceFile, TemplateAst, TemplateLoaderOrder}, vfs::{FileKind, VfsSnapshot}, FileId, }; @@ -69,16 +66,12 @@ impl FileStore { pub fn apply_vfs_snapshot(&mut self, snap: &VfsSnapshot) { for (id, rec) in &snap.files { let new_text = snap.get_text(*id).unwrap_or_else(|| Arc::::from("")); - let new_kind = match rec.meta.kind { - FileKind::Python => FileKindMini::Python, - FileKind::Template => FileKindMini::Template, - FileKind::Other => FileKindMini::Other, - }; + let new_kind = rec.meta.kind; if let Some(sf) = self.files.get(id) { // Update if changed — avoid touching Salsa when not needed 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 { sf.set_text(&mut self.db).to(new_text.clone()); @@ -100,7 +93,7 @@ impl FileStore { /// Get the file kind classification by its [`FileId`]. /// /// Returns `None` if the file is not tracked in the [`FileStore`]. - pub fn file_kind(&self, id: FileId) -> Option { + pub fn file_kind(&self, id: FileId) -> Option { self.files.get(&id).map(|sf| sf.kind(&self.db)) } diff --git a/crates/djls-workspace/src/db.rs b/crates/djls-workspace/src/db.rs index a38757d..1b4ece5 100644 --- a/crates/djls-workspace/src/db.rs +++ b/crates/djls-workspace/src/db.rs @@ -7,6 +7,8 @@ use std::sync::Arc; #[cfg(test)] use std::sync::Mutex; +use crate::vfs::FileKind; + /// Salsa database root for workspace /// /// The [`Database`] provides default storage and, in tests, captures Salsa events for @@ -49,21 +51,6 @@ impl Default for Database { #[salsa::db] 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. /// /// [`SourceFile`] is a Salsa input entity that tracks both the file's type (for routing @@ -72,7 +59,7 @@ pub enum FileKindMini { #[salsa::input] pub struct SourceFile { /// The file's classification for analysis routing - pub kind: FileKindMini, + pub kind: FileKind, /// The current text content of the file #[returns(ref)] pub text: Arc, @@ -113,7 +100,7 @@ pub struct TemplateAst { #[salsa::tracked] pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option> { // Only parse template files - if file.kind(db) != FileKindMini::Template { + if file.kind(db) != FileKind::Template { return None; } @@ -161,7 +148,7 @@ mod tests { // Create a template file let template_content: Arc = 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 let ast1 = parse_template(&db, file); @@ -181,7 +168,7 @@ mod tests { // Create a template file let template_content1: Arc = 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 let ast1 = parse_template(&db, file); @@ -206,7 +193,7 @@ mod tests { // Create a Python file let python_content: Arc = 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 let ast = parse_template(&db, file); @@ -223,7 +210,7 @@ mod tests { // Create a template with an error (unclosed tag) let template_content: Arc = 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 let errors1 = template_errors(&db, file); diff --git a/crates/djls-workspace/src/lib.rs b/crates/djls-workspace/src/lib.rs index 30c69a7..a3db4c3 100644 --- a/crates/djls-workspace/src/lib.rs +++ b/crates/djls-workspace/src/lib.rs @@ -5,8 +5,7 @@ mod watcher; pub use bridge::FileStore; pub use db::{ - parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, - TemplateLoaderOrder, + parse_template, template_errors, Database, SourceFile, TemplateAst, TemplateLoaderOrder, }; pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot}; pub use watcher::{VfsWatcher, WatchConfig, WatchEvent}; diff --git a/crates/djls-workspace/src/watcher.rs b/crates/djls-workspace/src/watcher.rs index 57c798c..55fa9a7 100644 --- a/crates/djls-workspace/src/watcher.rs +++ b/crates/djls-workspace/src/watcher.rs @@ -317,4 +317,3 @@ mod tests { assert_ne!(deleted, renamed); } } -