add new extention traits for Range and DocumentChange (#287)

This commit is contained in:
Josh Thomas 2025-10-03 11:28:01 -05:00 committed by GitHub
parent aa55cf0967
commit cdbe25d73c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 152 additions and 102 deletions

View file

@ -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<DocumentChange>;
}
impl TextDocumentContentChangeEventExt for Vec<lsp_types::TextDocumentContentChangeEvent> {
fn to_document_changes(self) -> Vec<DocumentChange> {
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<File>;
}

View file

@ -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(),
)?;

View file

@ -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();

View file

@ -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)]

View file

@ -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<Range>,
pub text: String,
range: Option<Range>,
text: String,
}
impl DocumentChange {
#[must_use]
pub fn new(range: Option<Range>, text: String) -> Self {
Self { range, text }
}
#[must_use]
pub fn range(&self) -> &Option<Range> {
&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");

View file

@ -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();