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 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<Arc<str>> {
vfs.get_text(self.file_id)
}
pub fn get_line(&self, vfs: &VfsSnapshot, line_index: &LineIndex, line: u32) -> Option<String> {
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<String> {
pub fn get_text_range(
&self,
vfs: &VfsSnapshot,
line_index: &LineIndex,
range: Range,
) -> Option<String> {
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<TemplateTagContext> {
pub fn get_template_tag_context(
&self,
vfs: &VfsSnapshot,
line_index: &LineIndex,
position: Position,
) -> Option<TemplateTagContext> {
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,
}

View file

@ -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(&current_content, &params.content_changes, line_index)?;
let new_content =
apply_text_changes(&current_content, &params.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");
}