lmntal-language-server 0.2.0

A language server for LMNtal.
Documentation
use lmntalc_ide::{
    Diagnostic as IdeDiagnostic, DiagnosticSeverity as IdeSeverity, DiagnosticStage as IdeStage,
    Pos, Source, Span,
};
use tower_lsp_server::ls_types::{
    Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, Position, Range, Uri,
};

pub fn to_lsp_diagnostics(
    source: &Source,
    uri: &Uri,
    diagnostics: &[IdeDiagnostic],
) -> Vec<Diagnostic> {
    diagnostics
        .iter()
        .map(|diagnostic| Diagnostic {
            range: diagnostic
                .primary_span
                .map(range_from_span)
                .unwrap_or_else(|| range_from_span(Span::default())),
            severity: Some(severity(diagnostic.severity)),
            source: Some(format!("lmntalc::{}", stage_name(diagnostic.stage))),
            message: format!("{}: {}", source.name(), diagnostic.message),
            related_information: if diagnostic.related_spans.is_empty() {
                None
            } else {
                Some(
                    diagnostic
                        .related_spans
                        .iter()
                        .map(|related| DiagnosticRelatedInformation {
                            location: Location {
                                uri: uri.clone(),
                                range: range_from_span(related.span),
                            },
                            message: related.message.clone(),
                        })
                        .collect(),
                )
            },
            ..Default::default()
        })
        .collect()
}

pub(crate) fn range_from_span(span: Span) -> Range {
    Range {
        start: position(span.low()),
        end: position(span.high()),
    }
}

fn position(pos: Pos) -> Position {
    Position {
        line: pos.line,
        character: pos.column,
    }
}

fn severity(severity: IdeSeverity) -> DiagnosticSeverity {
    match severity {
        IdeSeverity::Advice => DiagnosticSeverity::HINT,
        IdeSeverity::Warning => DiagnosticSeverity::WARNING,
        IdeSeverity::Error => DiagnosticSeverity::ERROR,
    }
}

fn stage_name(stage: IdeStage) -> &'static str {
    match stage {
        IdeStage::Lexing => "lexing",
        IdeStage::Parsing => "parsing",
        IdeStage::Semantics => "semantics",
        IdeStage::Lowering => "lowering",
        IdeStage::Backend => "backend",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use lmntalc_ide::{DiagnosticStage, RelatedSpan};

    #[test]
    fn converts_diagnostics_with_stage_and_related_information() {
        let source = Source::from_string("a :-".to_string());
        let diagnostics = vec![IdeDiagnostic {
            stage: DiagnosticStage::Parsing,
            severity: IdeSeverity::Error,
            message: "Unexpected end of file".to_string(),
            primary_span: Some(Span::new(Pos::new(2, 0, 2), Pos::new(4, 0, 4))),
            related_spans: vec![RelatedSpan {
                span: Span::new(Pos::new(0, 0, 0), Pos::new(1, 0, 1)),
                message: "Rule starts here".to_string(),
            }],
        }];

        let converted = to_lsp_diagnostics(
            &source,
            &"file:///test.lmn".parse().expect("uri should parse"),
            &diagnostics,
        );
        assert_eq!(converted.len(), 1);
        assert_eq!(converted[0].severity, Some(DiagnosticSeverity::ERROR));
        assert_eq!(converted[0].source.as_deref(), Some("lmntalc::parsing"));
        assert!(converted[0].related_information.is_some());
    }
}