1use daml_parser::ast::{DiagnosticCategory, Module, Span as ParserSpan};
9use daml_parser::layout::resolve_layout;
10use daml_parser::lexer::{lex_with_trivia, LexError, Token, Trivia};
11use daml_parser::parse::parse_module;
12use std::sync::OnceLock;
13
14pub use text_size::{TextRange, TextSize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct LineCol {
18 pub line: usize,
19 pub column: usize,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Diagnostic {
24 pub range: TextRange,
25 pub line: usize,
26 pub column: usize,
27 pub end_column: Option<usize>,
28 pub message: String,
29 pub category: DiagnosticCategory,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct LineIndex {
34 source_len: usize,
35 line_start_bytes: Vec<usize>,
36 utf16_offset_by_byte: Vec<usize>,
37}
38
39impl LineIndex {
40 #[must_use]
41 pub fn new(source: &str) -> Self {
42 let mut line_start_bytes = vec![0];
43 for (idx, byte) in source.bytes().enumerate() {
44 if byte == b'\n' {
45 line_start_bytes.push(idx + 1);
46 }
47 }
48
49 let mut utf16_offset_by_byte = vec![0; source.len() + 1];
50 let mut utf16 = 0usize;
51 let mut prev = 0usize;
52 for (idx, ch) in source.char_indices() {
53 for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
54 *slot = utf16;
55 }
56 let char_end = idx + ch.len_utf8();
57 for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
58 *slot = utf16;
59 }
60 utf16 += ch.len_utf16();
61 prev = char_end;
62 }
63 for slot in utf16_offset_by_byte
64 .iter_mut()
65 .take(source.len() + 1)
66 .skip(prev)
67 {
68 *slot = utf16;
69 }
70
71 Self {
72 source_len: source.len(),
73 line_start_bytes,
74 utf16_offset_by_byte,
75 }
76 }
77
78 #[must_use]
79 pub fn line_col(&self, offset: TextSize) -> LineCol {
80 let byte = usize::from(offset).min(self.source_len);
81 let line_idx = match self.line_start_bytes.binary_search(&byte) {
82 Ok(idx) => idx,
83 Err(idx) => idx.saturating_sub(1),
84 };
85 LineCol {
86 line: line_idx + 1,
87 column: byte - self.line_start_bytes[line_idx] + 1,
88 }
89 }
90
91 #[must_use]
92 pub fn char_line_col(&self, source: &str, offset: TextSize) -> LineCol {
93 let mut byte = usize::from(offset).min(self.source_len);
94 while !source.is_char_boundary(byte) {
95 byte = byte.saturating_sub(1);
96 }
97 let line_idx = match self.line_start_bytes.binary_search(&byte) {
98 Ok(idx) => idx,
99 Err(idx) => idx.saturating_sub(1),
100 };
101 let line_start = self.line_start_bytes[line_idx];
102 LineCol {
103 line: line_idx + 1,
104 column: source[line_start..byte].chars().count() + 1,
105 }
106 }
107
108 #[must_use]
109 pub fn utf16_col(&self, line: usize, byte_col: usize) -> usize {
110 let line_start = self
111 .line_start_bytes
112 .get(line.saturating_sub(1))
113 .copied()
114 .unwrap_or(self.source_len);
115 let byte = line_start
116 .saturating_add(byte_col.saturating_sub(1))
117 .min(self.source_len);
118 self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start]
119 }
120
121 #[must_use]
122 pub fn utf16_range(&self, range: TextRange) -> (usize, usize) {
123 let start = usize::from(range.start()).min(self.source_len);
124 let end = usize::from(range.end()).min(self.source_len).max(start);
125 (
126 self.utf16_offset_by_byte[start],
127 self.utf16_offset_by_byte[end],
128 )
129 }
130}
131
132#[derive(Debug)]
133pub struct SourceTokens {
134 tokens: Vec<Token>,
135 trivia: Vec<Trivia>,
136 lex_errors: Vec<LexError>,
137 laid_out_tokens: OnceLock<Vec<Token>>,
138}
139
140impl SourceTokens {
141 #[must_use]
142 pub fn lex(source: &str) -> Self {
143 let lexed = lex_with_trivia(source);
144 Self {
145 tokens: lexed.tokens,
146 trivia: lexed.trivia,
147 lex_errors: lexed.errors,
148 laid_out_tokens: OnceLock::new(),
149 }
150 }
151
152 #[must_use]
153 pub fn tokens(&self) -> &[Token] {
154 &self.tokens
155 }
156
157 #[must_use]
158 pub fn trivia(&self) -> &[Trivia] {
159 &self.trivia
160 }
161
162 #[must_use]
163 pub fn lex_errors(&self) -> &[LexError] {
164 &self.lex_errors
165 }
166
167 #[must_use]
168 pub fn laid_out_tokens(&self) -> &[Token] {
169 self.laid_out_tokens
170 .get_or_init(|| resolve_layout(self.tokens.clone()))
171 }
172}
173
174#[derive(Debug)]
175pub struct SourceFile {
176 source: String,
177 module: Module,
178 diagnostics: Vec<Diagnostic>,
179 line_index: LineIndex,
180 tokens: OnceLock<SourceTokens>,
181}
182
183impl SourceFile {
184 #[must_use]
185 pub fn parse(source: &str) -> Self {
186 let parsed = parse_module(source);
187 let line_index = LineIndex::new(source);
188 let diagnostics = parsed
189 .diagnostics
190 .into_iter()
191 .map(|diagnostic| {
192 let range = parser_span_to_text_range(source, diagnostic.span);
193 let start = range.start();
194 let end_column = source
195 .get(usize::from(range.start())..usize::from(range.end()))
196 .filter(|s| !s.is_empty() && !s.contains('\n'))
197 .map(|s| diagnostic.pos.column + s.chars().count());
198 Diagnostic {
199 range,
200 line: line_index.char_line_col(source, start).line,
201 column: diagnostic.pos.column,
202 end_column,
203 message: diagnostic.message,
204 category: diagnostic.category,
205 }
206 })
207 .collect();
208
209 Self {
210 source: source.to_string(),
211 module: parsed.module,
212 diagnostics,
213 line_index,
214 tokens: OnceLock::new(),
215 }
216 }
217
218 #[must_use]
219 pub fn source(&self) -> &str {
220 &self.source
221 }
222
223 #[must_use]
224 pub const fn module(&self) -> &Module {
225 &self.module
226 }
227
228 #[must_use]
229 pub fn diagnostics(&self) -> &[Diagnostic] {
230 &self.diagnostics
231 }
232
233 #[must_use]
234 pub const fn line_index(&self) -> &LineIndex {
235 &self.line_index
236 }
237
238 #[must_use]
239 pub fn tokens(&self) -> &[Token] {
240 self.source_tokens().tokens()
241 }
242
243 #[must_use]
244 pub fn trivia(&self) -> &[Trivia] {
245 self.source_tokens().trivia()
246 }
247
248 #[must_use]
249 pub fn laid_out_tokens(&self) -> &[Token] {
250 self.source_tokens().laid_out_tokens()
251 }
252
253 #[must_use]
254 pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
255 parser_span_to_text_range(&self.source, span)
256 }
257
258 fn source_tokens(&self) -> &SourceTokens {
259 self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
260 }
261}
262
263#[must_use]
264pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
265 let start = span.start.min(source.len());
266 let end = span.end.min(source.len()).max(start);
267 TextRange::new(
268 TextSize::try_from(start).unwrap(),
269 TextSize::try_from(end).unwrap(),
270 )
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use daml_parser::ast_span::render_from_ast;
277 use daml_parser::lexer::render_lossless;
278
279 #[test]
280 fn maps_empty_source_to_first_line() {
281 let index = LineIndex::new("");
282
283 assert_eq!(index.line_col(0.into()), LineCol { line: 1, column: 1 });
284 assert_eq!(index.utf16_range(TextRange::empty(0.into())), (0, 0));
285 }
286
287 #[test]
288 fn maps_ascii_byte_lines() {
289 let source = "module M where\nfoo = 1\n";
290 let index = LineIndex::new(source);
291
292 assert_eq!(index.line_col(15.into()), LineCol { line: 2, column: 1 });
293 assert_eq!(index.utf16_col(2, 4), 3);
294 }
295
296 #[test]
297 fn maps_utf8_and_utf16_offsets() {
298 let source = "a😀b\nz";
299 let index = LineIndex::new(source);
300
301 assert_eq!(
302 index.utf16_range(TextRange::new(0.into(), 6.into())),
303 (0, 4)
304 );
305 assert_eq!(index.utf16_col(1, 6), 3);
306 assert_eq!(
307 index.char_line_col(source, 5.into()),
308 LineCol { line: 1, column: 3 }
309 );
310 }
311
312 #[test]
313 fn char_line_col_snaps_to_previous_utf8_boundary() {
314 let source = "a😀b";
315 let index = LineIndex::new(source);
316
317 assert_eq!(
319 index.char_line_col(source, 3.into()),
320 LineCol { line: 1, column: 2 }
321 );
322 }
323
324 #[test]
325 fn preserves_trailing_newline_line_start() {
326 let index = LineIndex::new("a\n");
327
328 assert_eq!(index.line_col(2.into()), LineCol { line: 2, column: 1 });
329 }
330
331 #[test]
332 fn treats_crlf_as_bytes_without_normalization() {
333 let index = LineIndex::new("a\r\nb");
334
335 assert_eq!(index.line_col(3.into()), LineCol { line: 2, column: 1 });
336 }
337
338 #[test]
339 fn clamps_ranges_to_source_end() {
340 let index = LineIndex::new("abc");
341 let range = TextRange::new(1.into(), 99.into());
342
343 assert_eq!(index.utf16_range(range), (1, 3));
344 }
345
346 #[test]
347 fn source_file_exposes_parser_pipeline_facts() {
348 let source = "module M where\nfoo : Int\nfoo = 1\n";
349 let file = SourceFile::parse(source);
350
351 assert_eq!(file.source(), source);
352 assert_eq!(file.module().name, "M");
353 assert!(file.diagnostics().is_empty());
354 assert!(!file.tokens().is_empty());
355 assert!(!file.laid_out_tokens().is_empty());
356 assert_eq!(
357 render_lossless(source, file.tokens(), file.trivia()).as_deref(),
358 Ok(source)
359 );
360 assert_eq!(
361 render_from_ast(source, file.module(), file.trivia()).as_deref(),
362 Ok(source)
363 );
364 }
365
366 #[test]
367 fn source_tokens_exposes_lex_only_pipeline_facts() {
368 let source = "module M where\nfoo : Int\nfoo = 1\n";
369 let tokens = SourceTokens::lex(source);
370
371 assert!(tokens.lex_errors().is_empty());
372 assert!(!tokens.tokens().is_empty());
373 assert!(!tokens.laid_out_tokens().is_empty());
374 assert_eq!(
375 render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
376 Ok(source)
377 );
378 }
379
380 #[test]
381 fn malformed_source_keeps_source_file_and_diagnostics() {
382 let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
383
384 assert_eq!(file.module().name, "M");
385 assert!(file
386 .diagnostics()
387 .iter()
388 .any(|diagnostic| diagnostic.category == DiagnosticCategory::Lex));
389 }
390
391 #[test]
392 fn converts_parser_spans_to_text_ranges() {
393 let file = SourceFile::parse("module M where\nfoo = 1\n");
394 let range = file.parser_span_to_text_range(ParserSpan::new(0, 99));
395
396 assert_eq!(
397 range,
398 TextRange::new(0.into(), file.source().len().try_into().unwrap())
399 );
400 }
401}