Skip to content

Commit 141bd34

Browse files
unify and reorient line index/offset around File salsa input (#237)
1 parent c43baae commit 141bd34

File tree

38 files changed

+364
-471
lines changed

38 files changed

+364
-471
lines changed

crates/djls-ide/src/diagnostics.rs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use djls_semantic::ValidationError;
22
use djls_source::File;
3+
use djls_source::LineIndex;
34
use djls_source::Span;
4-
use djls_templates::LineOffsets;
55
use djls_templates::TemplateError;
66
use djls_templates::TemplateErrorAccumulator;
77
use tower_lsp_server::lsp_types;
@@ -56,35 +56,31 @@ impl DiagnosticError for ValidationError {
5656
}
5757

5858
/// Convert a Span to an LSP Range using line offsets.
59-
fn span_to_lsp_range(span: Span, line_offsets: &LineOffsets) -> lsp_types::Range {
60-
let start_pos = span.start as usize;
61-
let end_pos = (span.start + span.length) as usize;
62-
63-
let (start_line, start_char) = line_offsets.position_to_line_col(start_pos);
64-
let (end_line, end_char) = line_offsets.position_to_line_col(end_pos);
59+
fn span_to_lsp_range(span: Span, line_index: &LineIndex) -> lsp_types::Range {
60+
let (start_pos, end_pos) = span.to_line_col(line_index);
6561

6662
lsp_types::Range {
6763
start: lsp_types::Position {
68-
line: u32::try_from(start_line - 1).unwrap_or(u32::MAX), // LSP is 0-based, LineOffsets is 1-based
69-
character: u32::try_from(start_char).unwrap_or(u32::MAX),
64+
line: start_pos.line(),
65+
character: start_pos.column(),
7066
},
7167
end: lsp_types::Position {
72-
line: u32::try_from(end_line - 1).unwrap_or(u32::MAX),
73-
character: u32::try_from(end_char).unwrap_or(u32::MAX),
68+
line: end_pos.line(),
69+
character: end_pos.column(),
7470
},
7571
}
7672
}
7773

7874
/// Convert any error implementing `DiagnosticError` to an LSP diagnostic.
7975
fn error_to_diagnostic(
8076
error: &impl DiagnosticError,
81-
line_offsets: &LineOffsets,
77+
line_index: &LineIndex,
8278
) -> lsp_types::Diagnostic {
8379
let range = error
8480
.span()
8581
.map(|(start, length)| {
8682
let span = Span::new(start, length);
87-
span_to_lsp_range(span, line_offsets)
83+
span_to_lsp_range(span, line_index)
8884
})
8985
.unwrap_or_default();
9086

@@ -134,13 +130,10 @@ pub fn collect_diagnostics(
134130
let template_errors =
135131
djls_templates::parse_template::accumulated::<TemplateErrorAccumulator>(db, file);
136132

137-
let line_offsets = nodelist
138-
.as_ref()
139-
.map(|nl| nl.line_offsets(db).clone())
140-
.unwrap_or_default();
133+
let line_index = file.line_index(db);
141134

142135
for error_acc in template_errors {
143-
diagnostics.push(error_to_diagnostic(&error_acc.0, &line_offsets));
136+
diagnostics.push(error_to_diagnostic(&error_acc.0, line_index));
144137
}
145138

146139
if let Some(nodelist) = nodelist {
@@ -149,7 +142,7 @@ pub fn collect_diagnostics(
149142
>(db, nodelist);
150143

151144
for error_acc in validation_errors {
152-
diagnostics.push(error_to_diagnostic(&error_acc.0, &line_offsets));
145+
diagnostics.push(error_to_diagnostic(&error_acc.0, line_index));
153146
}
154147
}
155148

crates/djls-source/src/file.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use camino::Utf8Path;
55
use camino::Utf8PathBuf;
66

77
use crate::db::Db;
8+
use crate::position::LineIndex;
89

910
#[salsa::input]
1011
pub struct File {
@@ -27,14 +28,7 @@ impl File {
2728
#[salsa::tracked(returns(ref))]
2829
pub fn line_index(self, db: &dyn Db) -> LineIndex {
2930
let text = self.source(db);
30-
let mut starts = Vec::with_capacity(256);
31-
starts.push(0);
32-
for (i, b) in text.0.source.bytes().enumerate() {
33-
if b == b'\n' {
34-
starts.push(u32::try_from(i).unwrap_or_default() + 1);
35-
}
36-
}
37-
LineIndex(starts)
31+
LineIndex::from_text(text.0.source.as_str())
3832
}
3933
}
4034

@@ -123,6 +117,3 @@ impl FileKind {
123117
}
124118
}
125119
}
126-
127-
#[derive(Debug, Clone, PartialEq, Eq)]
128-
pub struct LineIndex(Vec<u32>);

crates/djls-source/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
mod db;
22
mod file;
3-
mod span;
3+
mod position;
44

55
pub use db::Db;
66
pub use file::File;
77
pub use file::FileKind;
8-
pub use span::Span;
8+
pub use position::ByteOffset;
9+
pub use position::LineCol;
10+
pub use position::LineIndex;
11+
pub use position::Span;

crates/djls-source/src/position.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use serde::Serialize;
2+
3+
/// A byte offset within a text document.
4+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
5+
pub struct ByteOffset(pub u32);
6+
7+
/// A line and column position within a text document.
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9+
pub struct LineCol(pub (u32, u32));
10+
11+
impl LineCol {
12+
#[must_use]
13+
pub fn line(&self) -> u32 {
14+
self.0 .0
15+
}
16+
17+
#[must_use]
18+
pub fn column(&self) -> u32 {
19+
self.0 .1
20+
}
21+
}
22+
23+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
24+
pub struct Span {
25+
pub start: u32,
26+
pub length: u32,
27+
}
28+
29+
impl Span {
30+
#[must_use]
31+
pub fn new(start: u32, length: u32) -> Self {
32+
Self { start, length }
33+
}
34+
35+
#[must_use]
36+
pub fn start_offset(&self) -> ByteOffset {
37+
ByteOffset(self.start)
38+
}
39+
40+
#[must_use]
41+
pub fn end_offset(&self) -> ByteOffset {
42+
ByteOffset(self.start.saturating_add(self.length))
43+
}
44+
45+
/// Convert this span to start and end line/column positions using the given line index.
46+
#[must_use]
47+
pub fn to_line_col(&self, line_index: &LineIndex) -> (LineCol, LineCol) {
48+
let start = line_index.to_line_col(self.start_offset());
49+
let end = line_index.to_line_col(self.end_offset());
50+
(start, end)
51+
}
52+
}
53+
54+
#[derive(Debug, Clone, PartialEq, Eq)]
55+
pub struct LineIndex(Vec<u32>);
56+
57+
impl LineIndex {
58+
#[must_use]
59+
pub fn from_text(text: &str) -> Self {
60+
let mut starts = Vec::with_capacity(256);
61+
starts.push(0);
62+
63+
let bytes = text.as_bytes();
64+
let mut i = 0;
65+
while i < bytes.len() {
66+
match bytes[i] {
67+
b'\n' => {
68+
// LF - Unix style line ending
69+
starts.push(u32::try_from(i + 1).unwrap_or_default());
70+
i += 1;
71+
}
72+
b'\r' => {
73+
// CR - check if followed by LF for Windows style
74+
if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
75+
// CRLF - Windows style line ending
76+
starts.push(u32::try_from(i + 2).unwrap_or_default());
77+
i += 2;
78+
} else {
79+
// Just CR - old Mac style line ending
80+
starts.push(u32::try_from(i + 1).unwrap_or_default());
81+
i += 1;
82+
}
83+
}
84+
_ => i += 1,
85+
}
86+
}
87+
88+
LineIndex(starts)
89+
}
90+
91+
#[must_use]
92+
pub fn to_line_col(&self, offset: ByteOffset) -> LineCol {
93+
if self.0.is_empty() {
94+
return LineCol((0, 0));
95+
}
96+
97+
let line = match self.0.binary_search(&offset.0) {
98+
Ok(exact) => exact,
99+
Err(0) => 0,
100+
Err(next) => next - 1,
101+
};
102+
103+
let line_start = self.0[line];
104+
let column = offset.0.saturating_sub(line_start);
105+
106+
LineCol((u32::try_from(line).unwrap_or_default(), column))
107+
}
108+
109+
#[must_use]
110+
pub fn line_start(&self, line: u32) -> Option<u32> {
111+
self.0.get(line as usize).copied()
112+
}
113+
114+
#[must_use]
115+
pub fn lines(&self) -> &[u32] {
116+
&self.0
117+
}
118+
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn test_line_index_unix_endings() {
126+
let text = "line1\nline2\nline3";
127+
let index = LineIndex::from_text(text);
128+
assert_eq!(index.lines(), &[0, 6, 12]);
129+
}
130+
131+
#[test]
132+
fn test_line_index_windows_endings() {
133+
let text = "line1\r\nline2\r\nline3";
134+
let index = LineIndex::from_text(text);
135+
// After "line1\r\n" (7 bytes), next line starts at byte 7
136+
// After "line2\r\n" (7 bytes), next line starts at byte 14
137+
assert_eq!(index.lines(), &[0, 7, 14]);
138+
}
139+
140+
#[test]
141+
fn test_line_index_mixed_endings() {
142+
let text = "line1\nline2\r\nline3\rline4";
143+
let index = LineIndex::from_text(text);
144+
// "line1\n" -> next at 6
145+
// "line2\r\n" -> next at 13
146+
// "line3\r" -> next at 19
147+
assert_eq!(index.lines(), &[0, 6, 13, 19]);
148+
}
149+
150+
#[test]
151+
fn test_line_index_empty() {
152+
let text = "";
153+
let index = LineIndex::from_text(text);
154+
assert_eq!(index.lines(), &[0]);
155+
}
156+
157+
#[test]
158+
fn test_to_line_col_with_crlf() {
159+
let text = "hello\r\nworld";
160+
let index = LineIndex::from_text(text);
161+
162+
// "hello" is 5 bytes, then \r\n, so "world" starts at byte 7
163+
assert_eq!(index.to_line_col(ByteOffset(0)), LineCol((0, 0)));
164+
assert_eq!(index.to_line_col(ByteOffset(7)), LineCol((1, 0)));
165+
assert_eq!(index.to_line_col(ByteOffset(8)), LineCol((1, 1)));
166+
}
167+
}

crates/djls-source/src/span.rs

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)