bynk_lsp/
explain_request.rs1use bynk_syntax::diagnostics;
31use bynk_syntax::span::Span;
32use tower_lsp::lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, Command};
33
34pub const EXPLAIN_COMMAND: &str = "bynk.explainDiagnostic";
38
39pub const CLIENT_CAPABILITY: &str = "explainDiagnosticCommand";
42
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45pub struct ExplainParams {
46 pub code: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51pub struct WireExplanation {
52 pub title: String,
54 pub markdown: String,
57 pub href: String,
60}
61
62pub fn explanation_for(code: &str) -> Option<WireExplanation> {
65 let e = diagnostics::explain(code)?;
66 let markdown = format!(
67 "{blurb}\n\n## Example\n\n```bynk\n{example}\n```\n",
68 blurb = e.blurb,
69 example = e.example.trim_end(),
70 );
71 Some(WireExplanation {
72 title: e.code.to_string(),
73 markdown,
74 href: e.href(),
75 })
76}
77
78pub fn client_supports_explain_command(experimental: Option<&serde_json::Value>) -> bool {
80 experimental
81 .and_then(|v| v.get(CLIENT_CAPABILITY))
82 .and_then(serde_json::Value::as_bool)
83 .unwrap_or(false)
84}
85
86pub fn explain_actions(
90 diagnostics: &[bynk_ide::Diagnostic],
91 requested: Span,
92) -> Vec<CodeActionOrCommand> {
93 let mut seen: Vec<&str> = Vec::new();
94 let mut out = Vec::new();
95 for d in diagnostics {
96 let code = d.error.category;
97 if !intersects(d.error.span, requested) || seen.contains(&code) {
98 continue;
99 }
100 if diagnostics::explain(code).is_none() {
101 continue;
102 }
103 seen.push(code);
104 let title = format!("Explain `{code}`");
105 out.push(CodeActionOrCommand::CodeAction(CodeAction {
106 title: title.clone(),
107 kind: Some(CodeActionKind::QUICKFIX),
108 command: Some(Command {
109 title,
110 command: EXPLAIN_COMMAND.to_string(),
111 arguments: Some(vec![serde_json::Value::String(code.to_string())]),
112 }),
113 is_preferred: Some(false),
114 ..Default::default()
115 }));
116 }
117 out
118}
119
120fn intersects(a: Span, b: Span) -> bool {
123 a.start <= b.end && b.start <= a.end
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn a_curated_code_has_title_markdown_and_href() {
132 let e = diagnostics::explain("bynk.resolve.unknown_type").unwrap();
133 let w = explanation_for("bynk.resolve.unknown_type").expect("curated");
134 assert_eq!(w.title, "bynk.resolve.unknown_type");
135 assert!(w.markdown.starts_with(e.blurb), "{}", w.markdown);
136 assert!(w.markdown.contains("```bynk\n"), "{}", w.markdown);
137 for line in e.example.lines() {
138 assert!(w.markdown.contains(line), "example line {line:?} missing");
139 }
140 assert_eq!(w.href, "https://bynk-lang.org/book/reference/types/");
141 }
142
143 #[test]
144 fn every_curated_code_explains() {
145 for e in diagnostics::EXPLANATIONS {
146 let w = explanation_for(e.code).expect("curated");
147 assert_eq!(w.href, e.href());
148 assert!(!w.markdown.contains('<'), "no raw HTML: {}", e.code);
149 }
150 }
151
152 #[test]
153 fn an_uncurated_or_unknown_code_is_none() {
154 assert!(diagnostics::lookup("bynk.resolve.duplicate_type").is_some());
155 assert!(explanation_for("bynk.resolve.duplicate_type").is_none());
156 assert!(explanation_for("bynk.not.a_real_code").is_none());
157 assert!(explanation_for("").is_none());
158 }
159
160 #[test]
161 fn the_client_capability_gate() {
162 assert!(!client_supports_explain_command(None));
163 assert!(!client_supports_explain_command(Some(&serde_json::json!(
164 {}
165 ))));
166 assert!(!client_supports_explain_command(Some(
167 &serde_json::json!({ "explainDiagnosticCommand": "yes" })
168 )));
169 assert!(client_supports_explain_command(Some(
170 &serde_json::json!({ "explainDiagnosticCommand": true })
171 )));
172 }
173
174 #[test]
175 fn params_deserialize_from_the_wire_shape() {
176 let p: ExplainParams =
177 serde_json::from_value(serde_json::json!({ "code": "bynk.resolve.unknown_name" }))
178 .unwrap();
179 assert_eq!(p.code, "bynk.resolve.unknown_name");
180 }
181}