bend_language_server/core/
diagnostics.rs

1use std::path::Path;
2
3pub use bend::diagnostics::*;
4use bend::{check_book, imports::DefaultLoader, CompileOpts};
5use tower_lsp::lsp_types::{self as lsp, Position};
6use tree_sitter as ts;
7
8use super::document::Document;
9use crate::utils::color_wrapper::treat_colors;
10
11/// Checks a Bend file and return its diagnostics.
12pub fn check(doc: &Document) -> Diagnostics {
13    let path = Path::new(doc.url.path());
14    let diagnostics_config = DiagnosticsConfig::new(Severity::Warning, true);
15    let compile_opts = CompileOpts::default();
16
17    let package_loader = DefaultLoader::new(path);
18
19    let diagnostics = bend::load_file_to_book(path, package_loader, diagnostics_config)
20        .and_then(|mut book| check_book(&mut book, diagnostics_config, compile_opts));
21
22    match diagnostics {
23        Ok(d) | Err(d) => d,
24    }
25}
26
27pub fn lsp_diagnostics(doc: &Document, diagnostics: &Diagnostics) -> Vec<lsp::Diagnostic> {
28    diagnostics
29        .diagnostics
30        // Iter<(DiagnosticOrigin, Vec<Diagnostic>)>
31        .iter()
32        // -> Iter<(DiagnosticOrigin, Diagnostic)>
33        .flat_map(|(key, vals)| vals.iter().map(move |val| (key, val)))
34        // Ignore unwanted diagnostics
35        .filter_map(|(origin, diagnostic)| treat_diagnostic(doc, origin, diagnostic))
36        .collect()
37}
38
39fn treat_diagnostic(
40    doc: &Document,
41    origin: &DiagnosticOrigin,
42    diag: &Diagnostic,
43) -> Option<lsp::Diagnostic> {
44    Some(lsp::Diagnostic {
45        message: treat_colors(&diag.display_with_origin(origin).to_string()),
46        severity: match diag.severity {
47            Severity::Allow => Some(lsp::DiagnosticSeverity::HINT),
48            Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
49            Severity::Error => Some(lsp::DiagnosticSeverity::ERROR),
50        },
51        range: match origin {
52            DiagnosticOrigin::Function(name) => find_def(doc, name.as_ref())?,
53            DiagnosticOrigin::Inet(name) => find_def(doc, name.as_ref())?,
54            _ => span_to_range(&diag.source.span),
55        },
56        code: None,
57        code_description: None,
58        source: Some("bend".into()),
59        related_information: None,
60        tags: None,
61        data: None,
62    })
63}
64
65/// Diagnostics with `Rule`, `Function` or `Inet` origins may have their
66/// spans including entire definitions, while we would only like to
67/// highlight their names.
68fn find_def(doc: &Document, name: &str) -> Option<lsp::Range> {
69    let query = format!(
70        r#"
71        (fun_function_definition
72            name: (identifier) @name
73            (#eq? @name "{name}"))
74        (imp_function_definition
75            name: (identifier) @name
76            (#eq? @name "{name}"))
77    "#
78    );
79
80    doc.find_one(&query)
81        .map(|node| ts_range_to_lsp(node.range()))
82}
83
84fn span_to_range(span: &Option<TextSpan>) -> lsp::Range {
85    span.as_ref()
86        .map(|span| lsp::Range {
87            start: Position {
88                line: span.start.line as u32,
89                character: span.start.char as u32,
90            },
91            end: Position {
92                line: span.end.line as u32,
93                character: span.end.char as u32,
94            },
95        })
96        .unwrap_or_default()
97}
98
99pub fn ts_range_to_lsp(range: ts::Range) -> lsp::Range {
100    lsp::Range {
101        start: lsp::Position {
102            line: range.start_point.row as u32,
103            character: range.start_point.column as u32,
104        },
105        end: lsp::Position {
106            line: range.end_point.row as u32,
107            character: range.end_point.column as u32,
108        },
109    }
110}