cairo_lang_diagnostics/
location_marks.rs

1use std::sync::Arc;
2
3use cairo_lang_filesystem::span::{FileSummary, TextPosition, TextSpan, TextWidth};
4use itertools::repeat_n;
5
6use crate::DiagnosticLocation;
7
8#[cfg(test)]
9#[path = "location_marks_test.rs"]
10mod test;
11
12/// Given a diagnostic location, returns a string with the location marks.
13pub fn get_location_marks(
14    db: &dyn cairo_lang_filesystem::db::FilesGroup,
15    location: &DiagnosticLocation,
16    skip_middle_lines: bool,
17) -> String {
18    let span = &location.span;
19    let summary = db.file_summary(location.file_id).expect("File missing from DB.");
20    let TextPosition { line: first_line_idx, col: _ } = span
21        .start
22        .position_in_file(db, location.file_id)
23        .expect("Failed to find location in file.");
24    if span.end.as_u32() <= summary.last_offset.as_u32() {
25        let TextPosition { line: last_line_idx, col: _ } = span
26            .end
27            .position_in_file(db, location.file_id)
28            .expect("Failed to find location in file.");
29        if first_line_idx != last_line_idx {
30            return get_multiple_lines_location_marks(db, location, skip_middle_lines);
31        }
32    }
33    get_single_line_location_marks(db, location)
34}
35
36/// Given a single line diagnostic location, returns a string with the location marks.
37fn get_single_line_location_marks(
38    db: &dyn cairo_lang_filesystem::db::FilesGroup,
39    location: &DiagnosticLocation,
40) -> String {
41    // TODO(ilya, 10/10/2023): Handle locations which spread over a few lines.
42    let content = db.file_content(location.file_id).expect("File missing from DB.");
43    let summary = db.file_summary(location.file_id).expect("File missing from DB.");
44    let span = &location.span;
45    let TextPosition { line: first_line_idx, col } = span
46        .start
47        .position_in_file(db, location.file_id)
48        .expect("Failed to find location in file.");
49    let first_line_start = summary.line_offsets[first_line_idx];
50    let first_line_end = match summary.line_offsets.get(first_line_idx + 1) {
51        Some(offset) => offset.sub_width(TextWidth::from_char('\n')),
52        None => summary.last_offset,
53    };
54
55    let first_line_span = TextSpan { start: first_line_start, end: first_line_end };
56    let mut res = first_line_span.take(&content).to_string();
57    res.push('\n');
58    res.extend(repeat_n(' ', col));
59    let subspan_in_first_line =
60        TextSpan { start: span.start, end: std::cmp::min(first_line_end, span.end) };
61    let marker_length = subspan_in_first_line.n_chars(&content);
62    // marker_length can be 0 if the span is empty.
63    res.extend(repeat_n('^', std::cmp::max(marker_length, 1)));
64
65    res
66}
67
68/// Given a multiple lines diagnostic location, returns a string with the location marks.
69fn get_multiple_lines_location_marks(
70    db: &dyn cairo_lang_filesystem::db::FilesGroup,
71    location: &DiagnosticLocation,
72    skip_middle_lines: bool,
73) -> String {
74    let content = db.file_content(location.file_id).expect("File missing from DB.");
75    let summary = db.file_summary(location.file_id).expect("File missing from DB.");
76
77    let span = &location.span;
78    let TextPosition { line: first_line_idx, col } = span
79        .start
80        .position_in_file(db, location.file_id)
81        .expect("Failed to find location in file.");
82    let mut res = get_line_content(summary.clone(), first_line_idx, content.clone(), true);
83    res += " _";
84    res.extend(repeat_n('_', col));
85    res += "^\n";
86    let TextPosition { line: last_line_idx, col: end_line_col } =
87        span.end.position_in_file(db, location.file_id).expect("Failed to find location in file.");
88
89    const LINES_TO_REPLACE_MIDDLE: usize = 3;
90    if !skip_middle_lines || first_line_idx + LINES_TO_REPLACE_MIDDLE > last_line_idx {
91        for row_index in first_line_idx + 1..=last_line_idx - 1 {
92            res += &get_line_content(summary.clone(), row_index, content.clone(), false);
93        }
94    } else {
95        res += "| ...\n";
96    }
97
98    res += &get_line_content(summary.clone(), last_line_idx, content.clone(), false);
99    res += "|";
100    res.extend(repeat_n('_', end_line_col));
101    res.push('^');
102
103    res
104}
105
106fn get_line_content(
107    summary: Arc<FileSummary>,
108    row_index: usize,
109    content: Arc<str>,
110    first_line: bool,
111) -> String {
112    let line_start = summary.line_offsets[row_index];
113    let line_end = match summary.line_offsets.get(row_index + 1) {
114        Some(offset) => offset.sub_width(TextWidth::from_char('\n')),
115        None => summary.last_offset,
116    };
117
118    let line_span = TextSpan { start: line_start, end: line_end };
119    format!("{}{}\n", if first_line { "  " } else { "| " }, line_span.take(&content))
120}