diff --git a/crates/djls-server/src/ext.rs b/crates/djls-server/src/ext.rs index 9268aa9..78163d3 100644 --- a/crates/djls-server/src/ext.rs +++ b/crates/djls-server/src/ext.rs @@ -5,18 +5,25 @@ use djls_source::LineCol; use djls_source::LineIndex; use djls_source::Offset; use djls_source::PositionEncoding; +use djls_source::Range; use djls_workspace::Db as WorkspaceDb; +use djls_workspace::DocumentChange; use tower_lsp_server::lsp_types; use tower_lsp_server::UriExt as TowerUriExt; pub(crate) trait PositionExt { - fn to_offset(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> Offset; + fn to_line_col(&self) -> LineCol; + fn to_offset(&self, text: &str, line_index: &LineIndex, encoding: PositionEncoding) -> Offset; } impl PositionExt for lsp_types::Position { - fn to_offset(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> Offset { - let line_col = LineCol::new(self.line, self.character); - index.offset(line_col, text, encoding) + fn to_line_col(&self) -> LineCol { + LineCol::new(self.line, self.character) + } + + fn to_offset(&self, text: &str, line_index: &LineIndex, encoding: PositionEncoding) -> Offset { + let line_col = self.to_line_col(); + line_index.offset(text, line_col, encoding) } } @@ -49,6 +56,32 @@ impl PositionEncodingKindExt for lsp_types::PositionEncodingKind { } } +pub(crate) trait RangeExt { + fn to_source_range(&self) -> Range; +} + +impl RangeExt for lsp_types::Range { + fn to_source_range(&self) -> Range { + let start_line_col = self.start.to_line_col(); + let end_line_col = self.end.to_line_col(); + Range::new(start_line_col, end_line_col) + } +} + +pub(crate) trait TextDocumentContentChangeEventExt { + fn to_document_changes(self) -> Vec; +} + +impl TextDocumentContentChangeEventExt for Vec { + fn to_document_changes(self) -> Vec { + self.into_iter() + .map(|change| { + DocumentChange::new(change.range.map(|r| r.to_source_range()), change.text) + }) + .collect() + } +} + pub(crate) trait TextDocumentIdentifierExt { fn to_file(&self, db: &mut dyn WorkspaceDb) -> Option; } diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 5eef9ee..7a5a7be 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -16,6 +16,7 @@ use tower_lsp_server::lsp_types; use crate::db::DjangoDatabase; use crate::ext::PositionEncodingKindExt; +use crate::ext::TextDocumentContentChangeEventExt; use crate::ext::UriExt; /// LSP Session managing project-specific state and database operations. @@ -155,21 +156,10 @@ impl Session { return None; }; - let doc_changes = changes - .into_iter() - .map(|change| djls_workspace::DocumentChange { - range: change.range.map(|r| djls_source::Range { - start: djls_source::LineCol::new(r.start.line, r.start.character), - end: djls_source::LineCol::new(r.end.line, r.end.character), - }), - text: change.text, - }) - .collect(); - let document = self.workspace.update_document( &mut self.db, &path, - doc_changes, + changes.to_document_changes(), text_document.version, self.client_capabilities.position_encoding(), )?; diff --git a/crates/djls-source/src/line.rs b/crates/djls-source/src/line.rs index 6d3bc6c..7e5b04f 100644 --- a/crates/djls-source/src/line.rs +++ b/crates/djls-source/src/line.rs @@ -88,7 +88,7 @@ impl LineIndex { } #[must_use] - pub fn offset(&self, line_col: LineCol, text: &str, encoding: PositionEncoding) -> Offset { + pub fn offset(&self, text: &str, line_col: LineCol, encoding: PositionEncoding) -> Offset { let line = line_col.line(); let character = line_col.column(); diff --git a/crates/djls-source/src/position.rs b/crates/djls-source/src/position.rs index 12e290e..65928c6 100644 --- a/crates/djls-source/src/position.rs +++ b/crates/djls-source/src/position.rs @@ -107,8 +107,25 @@ impl From<&LineCol> for (u32, u32) { } pub struct Range { - pub start: LineCol, - pub end: LineCol, + start: LineCol, + end: LineCol, +} + +impl Range { + #[must_use] + pub fn new(start: LineCol, end: LineCol) -> Self { + Self { start, end } + } + + #[must_use] + pub fn start(&self) -> LineCol { + self.start + } + + #[must_use] + pub fn end(&self) -> LineCol { + self.end + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] diff --git a/crates/djls-workspace/src/document.rs b/crates/djls-workspace/src/document.rs index da378a2..ee1ac7e 100644 --- a/crates/djls-workspace/src/document.rs +++ b/crates/djls-workspace/src/document.rs @@ -89,16 +89,7 @@ impl TextDocument { let mut line_index = self.line_index.clone(); for change in changes { - if let Some(range) = change.range { - let start_offset = - line_index.offset(range.start, &content, encoding).get() as usize; - let end_offset = line_index.offset(range.end, &content, encoding).get() as usize; - - content.replace_range(start_offset..end_offset, &change.text); - } else { - content = change.text; - } - + content = change.apply(&content, &line_index, encoding); line_index = LineIndex::from(content.as_str()); } @@ -109,8 +100,47 @@ impl TextDocument { } pub struct DocumentChange { - pub range: Option, - pub text: String, + range: Option, + text: String, +} + +impl DocumentChange { + #[must_use] + pub fn new(range: Option, text: String) -> Self { + Self { range, text } + } + + #[must_use] + pub fn range(&self) -> &Option { + &self.range + } + + #[must_use] + pub fn text(&self) -> &str { + &self.text + } + + /// Apply this change to content, returning the new content + #[must_use] + pub fn apply( + &self, + content: &str, + line_index: &LineIndex, + encoding: PositionEncoding, + ) -> String { + if let Some(range) = &self.range { + let start_offset = line_index.offset(content, range.start(), encoding).get() as usize; + let end_offset = line_index.offset(content, range.end(), encoding).get() as usize; + + let mut result = String::with_capacity(content.len() + self.text.len()); + result.push_str(&content[..start_offset]); + result.push_str(&self.text); + result.push_str(&content[end_offset..]); + result + } else { + self.text.clone() + } + } } #[cfg(test)] @@ -165,13 +195,10 @@ mod tests { fn test_incremental_update_single_change() { let mut doc = text_document("Hello world", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 6), - end: LineCol::new(0, 11), - }), - text: "Rust".to_string(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 6), LineCol::new(0, 11))), + "Rust".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Hello Rust"); @@ -183,20 +210,14 @@ mod tests { let mut doc = text_document("First line\nSecond line\nThird line", 1, LanguageId::Other); let changes = vec![ - DocumentChange { - range: Some(Range { - start: LineCol::new(0, 0), - end: LineCol::new(0, 5), - }), - text: "1st".to_string(), - }, - DocumentChange { - range: Some(Range { - start: LineCol::new(2, 0), - end: LineCol::new(2, 5), - }), - text: "3rd".to_string(), - }, + DocumentChange::new( + Some(Range::new(LineCol::new(0, 0), LineCol::new(0, 5))), + "1st".to_string(), + ), + DocumentChange::new( + Some(Range::new(LineCol::new(2, 0), LineCol::new(2, 5))), + "3rd".to_string(), + ), ]; doc.update(changes, 2, PositionEncoding::Utf16); @@ -207,13 +228,10 @@ mod tests { fn test_incremental_update_insertion() { let mut doc = text_document("Hello world", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 5), - end: LineCol::new(0, 5), - }), - text: " beautiful".to_string(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 5), LineCol::new(0, 5))), + " beautiful".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Hello beautiful world"); @@ -223,13 +241,10 @@ mod tests { fn test_incremental_update_deletion() { let mut doc = text_document("Hello beautiful world", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 6), - end: LineCol::new(0, 16), - }), - text: String::new(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 6), LineCol::new(0, 16))), + String::new(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Hello world"); @@ -239,10 +254,10 @@ mod tests { fn test_full_document_replacement() { let mut doc = text_document("Old content", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: None, - text: "Completely new content".to_string(), - }]; + let changes = vec![DocumentChange::new( + None, + "Completely new content".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Completely new content"); @@ -253,13 +268,10 @@ mod tests { fn test_incremental_update_multiline() { let mut doc = text_document("Line 1\nLine 2\nLine 3", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 5), - end: LineCol::new(2, 4), - }), - text: "A\nB\nC".to_string(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 5), LineCol::new(2, 4))), + "A\nB\nC".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Line A\nB\nC 3"); @@ -269,13 +281,10 @@ mod tests { fn test_incremental_update_with_emoji() { let mut doc = text_document("Hello 🌍 world", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 9), - end: LineCol::new(0, 14), - }), - text: "Rust".to_string(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 9), LineCol::new(0, 14))), + "Rust".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Hello 🌍 Rust"); @@ -285,13 +294,10 @@ mod tests { fn test_incremental_update_newline_at_end() { let mut doc = text_document("Hello", 1, LanguageId::Other); - let changes = vec![DocumentChange { - range: Some(Range { - start: LineCol::new(0, 5), - end: LineCol::new(0, 5), - }), - text: "\nWorld".to_string(), - }]; + let changes = vec![DocumentChange::new( + Some(Range::new(LineCol::new(0, 5), LineCol::new(0, 5))), + "\nWorld".to_string(), + )]; doc.update(changes, 2, PositionEncoding::Utf16); assert_eq!(doc.content(), "Hello\nWorld"); diff --git a/crates/djls-workspace/src/workspace.rs b/crates/djls-workspace/src/workspace.rs index 80729bc..bb50edc 100644 --- a/crates/djls-workspace/src/workspace.rs +++ b/crates/djls-workspace/src/workspace.rs @@ -101,10 +101,14 @@ impl Workspace { self.buffers.update(path.to_path_buf(), document.clone()); Some(document) } else if let Some(first_change) = changes.into_iter().next() { - if first_change.range.is_none() { + if first_change.range().is_none() { let file = db.get_or_create_file(path); - let document = - TextDocument::new(first_change.text, version, LanguageId::Other, file); + let document = TextDocument::new( + first_change.text().to_string(), + version, + LanguageId::Other, + file, + ); self.buffers.open(path.to_path_buf(), document.clone()); Some(document) } else { @@ -412,10 +416,10 @@ mod tests { let path = Utf8Path::new("/test.py"); workspace.open_document(&mut db, path, "initial", 1, "python"); - let changes = vec![crate::document::DocumentChange { - range: None, - text: "updated".to_string(), - }]; + let changes = vec![crate::document::DocumentChange::new( + None, + "updated".to_string(), + )]; let document = workspace .update_document(&mut db, path, changes, 2, PositionEncoding::Utf16) .unwrap(); @@ -496,10 +500,10 @@ mod tests { .open_document(&mut db, &file_path, "initial", 1, "python") .unwrap(); - let changes = vec![crate::document::DocumentChange { - range: None, - text: "updated".to_string(), - }]; + let changes = vec![crate::document::DocumentChange::new( + None, + "updated".to_string(), + )]; workspace .update_document(&mut db, &file_path, changes, 2, PositionEncoding::Utf16) .unwrap();