1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7use gruel_compiler::{JsonDiagnostic, JsonSpan, JsonSuggestion};
8use lsp_types::{
9 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString,
10 Position, Range, Url,
11};
12
13use crate::position::PositionEncoding;
14
15pub type DiagnosticsByFile = HashMap<PathBuf, Vec<Diagnostic>>;
17
18fn make_position(line: u32, column: u32) -> Position {
19 Position {
21 line: line.saturating_sub(1),
22 character: column.saturating_sub(1),
23 }
24}
25
26fn range_from_span(span: &JsonSpan, _encoding: PositionEncoding) -> Range {
27 Range {
34 start: make_position(span.line, span.column),
35 end: make_position(span.line, span.column),
36 }
37}
38
39pub fn to_lsp_diagnostic(
42 diag: &JsonDiagnostic,
43 workspace_root: Option<&Path>,
44) -> Option<(PathBuf, Diagnostic)> {
45 let primary = diag.spans.iter().find(|s| s.primary)?;
46 let range = range_from_span(primary, PositionEncoding::Utf8);
47
48 let severity = match diag.severity {
49 "error" => Some(DiagnosticSeverity::ERROR),
50 "warning" => Some(DiagnosticSeverity::WARNING),
51 _ => None,
52 };
53
54 let mut message = diag.message.clone();
55 for note in &diag.notes {
56 message.push_str("\nnote: ");
57 message.push_str(note);
58 }
59 for help in &diag.helps {
60 message.push_str("\nhelp: ");
61 message.push_str(help);
62 }
63
64 let related = diag
65 .spans
66 .iter()
67 .filter(|s| !s.primary)
68 .filter_map(|s| {
69 let path = resolve_path(&s.file, workspace_root)?;
70 let uri = Url::from_file_path(&path).ok()?;
71 Some(DiagnosticRelatedInformation {
72 location: Location {
73 uri,
74 range: range_from_span(s, PositionEncoding::Utf8),
75 },
76 message: s.label.clone().unwrap_or_default(),
77 })
78 })
79 .collect::<Vec<_>>();
80 let related = if related.is_empty() {
81 None
82 } else {
83 Some(related)
84 };
85
86 let path = resolve_path(&primary.file, workspace_root)?;
87
88 let code = if diag.code.is_empty() {
89 None
90 } else {
91 Some(NumberOrString::String(diag.code.clone()))
92 };
93
94 let suggestions_data = serde_json::to_value(&diag.suggestions).ok();
95
96 Some((
97 path,
98 Diagnostic {
99 range,
100 severity,
101 code,
102 code_description: None,
103 source: Some("gruel".to_string()),
104 message,
105 related_information: related,
106 tags: None,
107 data: suggestions_data,
108 },
109 ))
110}
111
112fn resolve_path(raw: &str, workspace_root: Option<&Path>) -> Option<PathBuf> {
113 let p = PathBuf::from(raw);
114 if p.is_absolute() {
115 return Some(p);
116 }
117 if let Some(root) = workspace_root {
118 return Some(root.join(p));
119 }
120 std::env::current_dir().ok().map(|cwd| cwd.join(p))
121}
122
123pub fn group_by_file(
125 diagnostics: impl IntoIterator<Item = JsonDiagnostic>,
126 workspace_root: Option<&Path>,
127) -> DiagnosticsByFile {
128 let mut out: DiagnosticsByFile = HashMap::new();
129 for d in diagnostics {
130 if let Some((path, diag)) = to_lsp_diagnostic(&d, workspace_root) {
131 out.entry(path).or_default().push(diag);
132 }
133 }
134 out
135}
136
137pub fn suggestions_from_diagnostic_data(value: &serde_json::Value) -> Vec<JsonSuggestion> {
140 serde_json::from_value(value.clone()).unwrap_or_default()
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use gruel_compiler::JsonSpan;
147
148 fn make_diag() -> JsonDiagnostic {
149 JsonDiagnostic {
150 code: "E0001".to_string(),
151 message: "type mismatch".to_string(),
152 severity: "error",
153 spans: vec![JsonSpan {
154 file: "main.gruel".to_string(),
155 start: 10,
156 end: 12,
157 line: 2,
158 column: 5,
159 label: None,
160 primary: true,
161 }],
162 suggestions: vec![],
163 notes: vec!["expected i32".to_string()],
164 helps: vec![],
165 }
166 }
167
168 #[test]
169 fn basic_mapping() {
170 let d = make_diag();
171 let (path, lsp) = to_lsp_diagnostic(&d, Some(Path::new("/work"))).unwrap();
172 assert_eq!(path, PathBuf::from("/work/main.gruel"));
173 assert_eq!(lsp.severity, Some(DiagnosticSeverity::ERROR));
174 assert!(lsp.message.contains("type mismatch"));
175 assert!(lsp.message.contains("note: expected i32"));
176 assert_eq!(
177 lsp.range.start,
178 Position {
179 line: 1,
180 character: 4
181 }
182 );
183 }
184}