Skip to main content

logicaffeine_lsp/
teach_md.rs

1//! Markdown rendering for the shared teaching brain
2//! (`logicaffeine_language::teach`) — the LSP's presentation of a lesson.
3//!
4//! One renderer feeds hover AND completion documentation, so the two can
5//! never phrase a lesson differently; the terminal REPL renders the same
6//! lessons in ANSI. The quickguide link goes through [`guide_url`] — one
7//! function to change if the guide ever moves off the repository.
8
9use tower_lsp::lsp_types::{Documentation, MarkupContent, MarkupKind};
10
11use logicaffeine_language::teach::ConstructDoc;
12
13/// The canonical "read more" URL for a quickguide heading slug.
14pub fn guide_url(anchor: &str) -> String {
15    format!(
16        "https://github.com/Brahmastra-Labs/logicaffeine/blob/main/LOGOS_QUICKGUIDE.md#{anchor}"
17    )
18}
19
20/// The full lesson as hover markdown: name, one plain sentence, the runnable
21/// example, the socratic question or tip, and the guide link when one fits.
22pub fn keyword_hover_md(doc: &ConstructDoc) -> String {
23    let mut md = format!(
24        "**{}**\n\n{}\n\n```\n{}\n```\n\n{}",
25        doc.name, doc.what, doc.example, doc.question_or_tip
26    );
27    push_guide_link(&mut md, doc);
28    md
29}
30
31/// The lesson for a `##` block header, keeping the Block Header banner.
32pub fn block_hover_md(doc: &ConstructDoc) -> String {
33    let mut md = format!(
34        "**Block Header** — {}\n\n```\n{}\n```\n\n{}",
35        doc.what, doc.example, doc.question_or_tip
36    );
37    push_guide_link(&mut md, doc);
38    md
39}
40
41/// The lesson as completion-item documentation (same rendering as hover —
42/// the editor's docs panel and the hover card always agree).
43pub fn completion_docs(doc: &ConstructDoc) -> Documentation {
44    Documentation::MarkupContent(MarkupContent {
45        kind: MarkupKind::Markdown,
46        value: keyword_hover_md(doc),
47    })
48}
49
50fn push_guide_link(md: &mut String, doc: &ConstructDoc) {
51    if let Some(anchor) = doc.guide_anchor {
52        md.push_str(&format!("\n\n[Quick Guide]({})", guide_url(anchor)));
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use logicaffeine_language::teach::{doc_for, doc_for_block};
60    use logicaffeine_language::token::{BlockType, TokenType};
61
62    #[test]
63    fn keyword_rendering_carries_every_lesson_part() {
64        let lesson = doc_for(&TokenType::Give).unwrap();
65        let md = keyword_hover_md(lesson);
66        assert!(md.contains("**Give**"), "{md}");
67        assert!(md.contains(lesson.what), "{md}");
68        assert!(md.contains(lesson.example), "{md}");
69        assert!(md.contains(lesson.question_or_tip), "{md}");
70        assert!(md.contains("LOGOS_QUICKGUIDE.md#13-output"), "{md}");
71    }
72
73    #[test]
74    fn block_rendering_keeps_the_banner() {
75        let lesson = doc_for_block(&BlockType::Main);
76        let md = block_hover_md(lesson);
77        assert!(md.starts_with("**Block Header** — "), "{md}");
78        assert!(md.contains(lesson.question_or_tip), "{md}");
79    }
80
81    #[test]
82    fn lessons_without_anchors_render_without_links() {
83        let lesson = doc_for(&TokenType::Escape).unwrap();
84        let md = keyword_hover_md(lesson);
85        assert!(!md.contains("Quick Guide"), "Escape has no guide section: {md}");
86    }
87
88    #[test]
89    fn completion_docs_match_hover_exactly() {
90        let lesson = doc_for(&TokenType::Let).unwrap();
91        let Documentation::MarkupContent(content) = completion_docs(lesson) else {
92            panic!("expected markup docs");
93        };
94        assert_eq!(content.kind, MarkupKind::Markdown);
95        assert_eq!(content.value, keyword_hover_md(lesson));
96    }
97}