Skip to content

Commit cdbe25d

Browse files
add new extention traits for Range and DocumentChange (#287)
1 parent aa55cf0 commit cdbe25d

File tree

6 files changed

+152
-102
lines changed

6 files changed

+152
-102
lines changed

crates/djls-server/src/ext.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ use djls_source::LineCol;
55
use djls_source::LineIndex;
66
use djls_source::Offset;
77
use djls_source::PositionEncoding;
8+
use djls_source::Range;
89
use djls_workspace::Db as WorkspaceDb;
10+
use djls_workspace::DocumentChange;
911
use tower_lsp_server::lsp_types;
1012
use tower_lsp_server::UriExt as TowerUriExt;
1113

1214
pub(crate) trait PositionExt {
13-
fn to_offset(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> Offset;
15+
fn to_line_col(&self) -> LineCol;
16+
fn to_offset(&self, text: &str, line_index: &LineIndex, encoding: PositionEncoding) -> Offset;
1417
}
1518

1619
impl PositionExt for lsp_types::Position {
17-
fn to_offset(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> Offset {
18-
let line_col = LineCol::new(self.line, self.character);
19-
index.offset(line_col, text, encoding)
20+
fn to_line_col(&self) -> LineCol {
21+
LineCol::new(self.line, self.character)
22+
}
23+
24+
fn to_offset(&self, text: &str, line_index: &LineIndex, encoding: PositionEncoding) -> Offset {
25+
let line_col = self.to_line_col();
26+
line_index.offset(text, line_col, encoding)
2027
}
2128
}
2229

@@ -49,6 +56,32 @@ impl PositionEncodingKindExt for lsp_types::PositionEncodingKind {
4956
}
5057
}
5158

59+
pub(crate) trait RangeExt {
60+
fn to_source_range(&self) -> Range;
61+
}
62+
63+
impl RangeExt for lsp_types::Range {
64+
fn to_source_range(&self) -> Range {
65+
let start_line_col = self.start.to_line_col();
66+
let end_line_col = self.end.to_line_col();
67+
Range::new(start_line_col, end_line_col)
68+
}
69+
}
70+
71+
pub(crate) trait TextDocumentContentChangeEventExt {
72+
fn to_document_changes(self) -> Vec<DocumentChange>;
73+
}
74+
75+
impl TextDocumentContentChangeEventExt for Vec<lsp_types::TextDocumentContentChangeEvent> {
76+
fn to_document_changes(self) -> Vec<DocumentChange> {
77+
self.into_iter()
78+
.map(|change| {
79+
DocumentChange::new(change.range.map(|r| r.to_source_range()), change.text)
80+
})
81+
.collect()
82+
}
83+
}
84+
5285
pub(crate) trait TextDocumentIdentifierExt {
5386
fn to_file(&self, db: &mut dyn WorkspaceDb) -> Option<File>;
5487
}

crates/djls-server/src/session.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use tower_lsp_server::lsp_types;
1616

1717
use crate::db::DjangoDatabase;
1818
use crate::ext::PositionEncodingKindExt;
19+
use crate::ext::TextDocumentContentChangeEventExt;
1920
use crate::ext::UriExt;
2021

2122
/// LSP Session managing project-specific state and database operations.
@@ -155,21 +156,10 @@ impl Session {
155156
return None;
156157
};
157158

158-
let doc_changes = changes
159-
.into_iter()
160-
.map(|change| djls_workspace::DocumentChange {
161-
range: change.range.map(|r| djls_source::Range {
162-
start: djls_source::LineCol::new(r.start.line, r.start.character),
163-
end: djls_source::LineCol::new(r.end.line, r.end.character),
164-
}),
165-
text: change.text,
166-
})
167-
.collect();
168-
169159
let document = self.workspace.update_document(
170160
&mut self.db,
171161
&path,
172-
doc_changes,
162+
changes.to_document_changes(),
173163
text_document.version,
174164
self.client_capabilities.position_encoding(),
175165
)?;

crates/djls-source/src/line.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ impl LineIndex {
8888
}
8989

9090
#[must_use]
91-
pub fn offset(&self, line_col: LineCol, text: &str, encoding: PositionEncoding) -> Offset {
91+
pub fn offset(&self, text: &str, line_col: LineCol, encoding: PositionEncoding) -> Offset {
9292
let line = line_col.line();
9393
let character = line_col.column();
9494

crates/djls-source/src/position.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,25 @@ impl From<&LineCol> for (u32, u32) {
107107
}
108108

109109
pub struct Range {
110-
pub start: LineCol,
111-
pub end: LineCol,
110+
start: LineCol,
111+
end: LineCol,
112+
}
113+
114+
impl Range {
115+
#[must_use]
116+
pub fn new(start: LineCol, end: LineCol) -> Self {
117+
Self { start, end }
118+
}
119+
120+
#[must_use]
121+
pub fn start(&self) -> LineCol {
122+
self.start
123+
}
124+
125+
#[must_use]
126+
pub fn end(&self) -> LineCol {
127+
self.end
128+
}
112129
}
113130

114131
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]

crates/djls-workspace/src/document.rs

Lines changed: 78 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,7 @@ impl TextDocument {
8989
let mut line_index = self.line_index.clone();
9090

9191
for change in changes {
92-
if let Some(range) = change.range {
93-
let start_offset =
94-
line_index.offset(range.start, &content, encoding).get() as usize;
95-
let end_offset = line_index.offset(range.end, &content, encoding).get() as usize;
96-
97-
content.replace_range(start_offset..end_offset, &change.text);
98-
} else {
99-
content = change.text;
100-
}
101-
92+
content = change.apply(&content, &line_index, encoding);
10293
line_index = LineIndex::from(content.as_str());
10394
}
10495

@@ -109,8 +100,47 @@ impl TextDocument {
109100
}
110101

111102
pub struct DocumentChange {
112-
pub range: Option<Range>,
113-
pub text: String,
103+
range: Option<Range>,
104+
text: String,
105+
}
106+
107+
impl DocumentChange {
108+
#[must_use]
109+
pub fn new(range: Option<Range>, text: String) -> Self {
110+
Self { range, text }
111+
}
112+
113+
#[must_use]
114+
pub fn range(&self) -> &Option<Range> {
115+
&self.range
116+
}
117+
118+
#[must_use]
119+
pub fn text(&self) -> &str {
120+
&self.text
121+
}
122+
123+
/// Apply this change to content, returning the new content
124+
#[must_use]
125+
pub fn apply(
126+
&self,
127+
content: &str,
128+
line_index: &LineIndex,
129+
encoding: PositionEncoding,
130+
) -> String {
131+
if let Some(range) = &self.range {
132+
let start_offset = line_index.offset(content, range.start(), encoding).get() as usize;
133+
let end_offset = line_index.offset(content, range.end(), encoding).get() as usize;
134+
135+
let mut result = String::with_capacity(content.len() + self.text.len());
136+
result.push_str(&content[..start_offset]);
137+
result.push_str(&self.text);
138+
result.push_str(&content[end_offset..]);
139+
result
140+
} else {
141+
self.text.clone()
142+
}
143+
}
114144
}
115145

116146
#[cfg(test)]
@@ -165,13 +195,10 @@ mod tests {
165195
fn test_incremental_update_single_change() {
166196
let mut doc = text_document("Hello world", 1, LanguageId::Other);
167197

168-
let changes = vec![DocumentChange {
169-
range: Some(Range {
170-
start: LineCol::new(0, 6),
171-
end: LineCol::new(0, 11),
172-
}),
173-
text: "Rust".to_string(),
174-
}];
198+
let changes = vec![DocumentChange::new(
199+
Some(Range::new(LineCol::new(0, 6), LineCol::new(0, 11))),
200+
"Rust".to_string(),
201+
)];
175202

176203
doc.update(changes, 2, PositionEncoding::Utf16);
177204
assert_eq!(doc.content(), "Hello Rust");
@@ -183,20 +210,14 @@ mod tests {
183210
let mut doc = text_document("First line\nSecond line\nThird line", 1, LanguageId::Other);
184211

185212
let changes = vec![
186-
DocumentChange {
187-
range: Some(Range {
188-
start: LineCol::new(0, 0),
189-
end: LineCol::new(0, 5),
190-
}),
191-
text: "1st".to_string(),
192-
},
193-
DocumentChange {
194-
range: Some(Range {
195-
start: LineCol::new(2, 0),
196-
end: LineCol::new(2, 5),
197-
}),
198-
text: "3rd".to_string(),
199-
},
213+
DocumentChange::new(
214+
Some(Range::new(LineCol::new(0, 0), LineCol::new(0, 5))),
215+
"1st".to_string(),
216+
),
217+
DocumentChange::new(
218+
Some(Range::new(LineCol::new(2, 0), LineCol::new(2, 5))),
219+
"3rd".to_string(),
220+
),
200221
];
201222

202223
doc.update(changes, 2, PositionEncoding::Utf16);
@@ -207,13 +228,10 @@ mod tests {
207228
fn test_incremental_update_insertion() {
208229
let mut doc = text_document("Hello world", 1, LanguageId::Other);
209230

210-
let changes = vec![DocumentChange {
211-
range: Some(Range {
212-
start: LineCol::new(0, 5),
213-
end: LineCol::new(0, 5),
214-
}),
215-
text: " beautiful".to_string(),
216-
}];
231+
let changes = vec![DocumentChange::new(
232+
Some(Range::new(LineCol::new(0, 5), LineCol::new(0, 5))),
233+
" beautiful".to_string(),
234+
)];
217235

218236
doc.update(changes, 2, PositionEncoding::Utf16);
219237
assert_eq!(doc.content(), "Hello beautiful world");
@@ -223,13 +241,10 @@ mod tests {
223241
fn test_incremental_update_deletion() {
224242
let mut doc = text_document("Hello beautiful world", 1, LanguageId::Other);
225243

226-
let changes = vec![DocumentChange {
227-
range: Some(Range {
228-
start: LineCol::new(0, 6),
229-
end: LineCol::new(0, 16),
230-
}),
231-
text: String::new(),
232-
}];
244+
let changes = vec![DocumentChange::new(
245+
Some(Range::new(LineCol::new(0, 6), LineCol::new(0, 16))),
246+
String::new(),
247+
)];
233248

234249
doc.update(changes, 2, PositionEncoding::Utf16);
235250
assert_eq!(doc.content(), "Hello world");
@@ -239,10 +254,10 @@ mod tests {
239254
fn test_full_document_replacement() {
240255
let mut doc = text_document("Old content", 1, LanguageId::Other);
241256

242-
let changes = vec![DocumentChange {
243-
range: None,
244-
text: "Completely new content".to_string(),
245-
}];
257+
let changes = vec![DocumentChange::new(
258+
None,
259+
"Completely new content".to_string(),
260+
)];
246261

247262
doc.update(changes, 2, PositionEncoding::Utf16);
248263
assert_eq!(doc.content(), "Completely new content");
@@ -253,13 +268,10 @@ mod tests {
253268
fn test_incremental_update_multiline() {
254269
let mut doc = text_document("Line 1\nLine 2\nLine 3", 1, LanguageId::Other);
255270

256-
let changes = vec![DocumentChange {
257-
range: Some(Range {
258-
start: LineCol::new(0, 5),
259-
end: LineCol::new(2, 4),
260-
}),
261-
text: "A\nB\nC".to_string(),
262-
}];
271+
let changes = vec![DocumentChange::new(
272+
Some(Range::new(LineCol::new(0, 5), LineCol::new(2, 4))),
273+
"A\nB\nC".to_string(),
274+
)];
263275

264276
doc.update(changes, 2, PositionEncoding::Utf16);
265277
assert_eq!(doc.content(), "Line A\nB\nC 3");
@@ -269,13 +281,10 @@ mod tests {
269281
fn test_incremental_update_with_emoji() {
270282
let mut doc = text_document("Hello 🌍 world", 1, LanguageId::Other);
271283

272-
let changes = vec![DocumentChange {
273-
range: Some(Range {
274-
start: LineCol::new(0, 9),
275-
end: LineCol::new(0, 14),
276-
}),
277-
text: "Rust".to_string(),
278-
}];
284+
let changes = vec![DocumentChange::new(
285+
Some(Range::new(LineCol::new(0, 9), LineCol::new(0, 14))),
286+
"Rust".to_string(),
287+
)];
279288

280289
doc.update(changes, 2, PositionEncoding::Utf16);
281290
assert_eq!(doc.content(), "Hello 🌍 Rust");
@@ -285,13 +294,10 @@ mod tests {
285294
fn test_incremental_update_newline_at_end() {
286295
let mut doc = text_document("Hello", 1, LanguageId::Other);
287296

288-
let changes = vec![DocumentChange {
289-
range: Some(Range {
290-
start: LineCol::new(0, 5),
291-
end: LineCol::new(0, 5),
292-
}),
293-
text: "\nWorld".to_string(),
294-
}];
297+
let changes = vec![DocumentChange::new(
298+
Some(Range::new(LineCol::new(0, 5), LineCol::new(0, 5))),
299+
"\nWorld".to_string(),
300+
)];
295301

296302
doc.update(changes, 2, PositionEncoding::Utf16);
297303
assert_eq!(doc.content(), "Hello\nWorld");

crates/djls-workspace/src/workspace.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,14 @@ impl Workspace {
101101
self.buffers.update(path.to_path_buf(), document.clone());
102102
Some(document)
103103
} else if let Some(first_change) = changes.into_iter().next() {
104-
if first_change.range.is_none() {
104+
if first_change.range().is_none() {
105105
let file = db.get_or_create_file(path);
106-
let document =
107-
TextDocument::new(first_change.text, version, LanguageId::Other, file);
106+
let document = TextDocument::new(
107+
first_change.text().to_string(),
108+
version,
109+
LanguageId::Other,
110+
file,
111+
);
108112
self.buffers.open(path.to_path_buf(), document.clone());
109113
Some(document)
110114
} else {
@@ -412,10 +416,10 @@ mod tests {
412416
let path = Utf8Path::new("/test.py");
413417
workspace.open_document(&mut db, path, "initial", 1, "python");
414418

415-
let changes = vec![crate::document::DocumentChange {
416-
range: None,
417-
text: "updated".to_string(),
418-
}];
419+
let changes = vec![crate::document::DocumentChange::new(
420+
None,
421+
"updated".to_string(),
422+
)];
419423
let document = workspace
420424
.update_document(&mut db, path, changes, 2, PositionEncoding::Utf16)
421425
.unwrap();
@@ -496,10 +500,10 @@ mod tests {
496500
.open_document(&mut db, &file_path, "initial", 1, "python")
497501
.unwrap();
498502

499-
let changes = vec![crate::document::DocumentChange {
500-
range: None,
501-
text: "updated".to_string(),
502-
}];
503+
let changes = vec![crate::document::DocumentChange::new(
504+
None,
505+
"updated".to_string(),
506+
)];
503507
workspace
504508
.update_document(&mut db, &file_path, changes, 2, PositionEncoding::Utf16)
505509
.unwrap();

0 commit comments

Comments
 (0)