merman-lsp 0.8.0-alpha.3

Language server for Merman diagnostics and completion foundations.
Documentation
use crate::diagnostics::analysis_payload_to_diagnostics;
use merman_analysis::{
    AnalysisDiagnostic, AnalysisPayload, Analyzer, DiagnosticCategory, DiagnosticSeverity,
    SourceDescriptor,
};
use tower_lsp::lsp_types::{DiagnosticTag, NumberOrString, Url};

#[test]
fn diagnostics_projection_preserves_uri_and_message() {
    let payload = AnalysisPayload::new(
        SourceDescriptor::diagram(),
        vec![AnalysisDiagnostic::error(
            "merman.parse.no_diagram",
            DiagnosticCategory::Parse,
            "no Mermaid diagram detected",
        )],
    );
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert_eq!(diagnostics.len(), 1);
    assert_eq!(diagnostics[0].message, "no Mermaid diagram detected");
    assert_eq!(
        diagnostics[0].code,
        Some(NumberOrString::String(
            "merman.parse.no_diagram".to_string()
        ))
    );
    assert_eq!(
        diagnostics[0]
            .code_description
            .as_ref()
            .expect("code description")
            .href
            .as_str(),
        "https://github.com/Latias94/merman/blob/main/docs/lsp/DIAGNOSTIC_PROTOCOL.md#canonical-rules"
    );
}

#[test]
fn diagnostics_projection_accepts_markdown_file_urls() {
    let payload = AnalysisPayload::new(
        SourceDescriptor::diagram(),
        vec![AnalysisDiagnostic::error(
            "merman.parse.no_diagram",
            DiagnosticCategory::Parse,
            "no Mermaid diagram detected",
        )],
    );
    let uri = Url::parse("file:///tmp/example.md").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert_eq!(diagnostics.len(), 1);
}

#[test]
fn diagnostics_projection_preserves_analysis_messages_verbatim() {
    let payload = AnalysisPayload::new(
        SourceDescriptor::diagram(),
        vec![AnalysisDiagnostic::error(
            "merman.parse.recovered_editor_facts",
            DiagnosticCategory::Parse,
            "flowchart parser recovered after parse error: unexpected statement separator",
        )],
    );
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert_eq!(diagnostics.len(), 1);
    assert_eq!(
        diagnostics[0].message,
        "flowchart parser recovered after parse error: unexpected statement separator"
    );
}

#[test]
fn flowchart_parse_recovery_does_not_duplicate_lsp_diagnostics() {
    let payload = Analyzer::new().analyze("flowchart TD\nA[unterminated");
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert_eq!(diagnostics.len(), 1);
    assert_eq!(
        diagnostics[0].message,
        "Unterminated node label (missing `]`)"
    );
    assert_eq!(
        diagnostics[0].code,
        Some(NumberOrString::String(
            "merman.parse.diagram_parse".to_string()
        ))
    );
}

#[test]
fn class_and_er_parse_spans_project_to_lsp_diagnostics() {
    let cases = [
        ("classDiagram\nA <|--", "class parse"),
        ("erDiagram\nCUSTOMER ||--o{ ORDER :", "er parse"),
    ];
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();

    for (source, label) in cases {
        let payload = Analyzer::new().analyze(source);
        let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

        assert_eq!(diagnostics.len(), 1, "{label}");
        assert_eq!(
            diagnostics[0].code,
            Some(NumberOrString::String(
                "merman.parse.diagram_parse".to_string()
            )),
            "{label}"
        );
        assert!(
            diagnostics[0].range.start.line > 0 || diagnostics[0].range.start.character > 0,
            "{label} should not default to the document start"
        );
    }
}

#[test]
fn diagnostics_projection_preserves_rule_metadata_in_data() {
    let payload = Analyzer::new().analyze("flowchart TD\nA-->B\n");
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert!(diagnostics.is_empty());

    let payload = AnalysisPayload::new(
        SourceDescriptor::diagram(),
        vec![
            AnalysisDiagnostic::new(
                "merman.test.info",
                DiagnosticSeverity::Hint,
                DiagnosticCategory::Config,
                "test metadata",
            )
            .with_diagram_type("flowchart-v2")
            .with_help("See rule docs."),
        ],
    );
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);
    let data = diagnostics[0].data.as_ref().expect("diagnostic data");

    assert_eq!(data["id"], "merman.test.info");
    assert_eq!(data["category"], "config");
    assert_eq!(data["diagramType"], "flowchart-v2");
    assert_eq!(data["help"], "See rule docs.");
}

#[test]
fn deprecated_diagnostics_project_lsp_tag() {
    let payload = AnalysisPayload::new(
        SourceDescriptor::diagram(),
        vec![AnalysisDiagnostic::new(
            "merman.compatibility.config.deprecated_flowchart_html_labels",
            DiagnosticSeverity::Warning,
            DiagnosticCategory::Config,
            "deprecated option",
        )],
    );
    let uri = Url::parse("file:///tmp/example.mmd").unwrap();
    let diagnostics = analysis_payload_to_diagnostics(&payload, &uri);

    assert_eq!(diagnostics[0].tags, Some(vec![DiagnosticTag::DEPRECATED]));
}