ts_bridge/protocol/
diagnostics.rs1use lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Uri};
2use serde_json::{Value, json};
3
4use crate::protocol::NotificationSpec;
5use crate::rpc::{Priority, Route};
6use crate::utils::{file_path_to_uri, tsserver_range_from_value_lsp};
7
8const REQUEST_COMPLETED: &str = "requestCompleted";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DiagnosticsKind {
12 Syntax,
13 Semantic,
14 Suggestion,
15}
16
17impl DiagnosticsKind {
18 fn from_event_name(name: &str) -> Option<Self> {
19 match name {
20 "syntaxDiag" => Some(DiagnosticsKind::Syntax),
21 "semanticDiag" => Some(DiagnosticsKind::Semantic),
22 "suggestionDiag" => Some(DiagnosticsKind::Suggestion),
23 _ => None,
24 }
25 }
26}
27
28#[derive(Debug)]
29pub enum DiagnosticsEvent {
30 Report {
31 kind: DiagnosticsKind,
32 request_seq: Option<u64>,
33 uri: Uri,
34 diagnostics: Vec<Diagnostic>,
35 },
36 Completed {
37 request_seq: u64,
38 },
39}
40
41pub fn request_for_file(file: &str) -> NotificationSpec {
42 let payload = json!({
43 "command": "geterr",
44 "arguments": {
45 "files": [file],
46 "delay": 0,
47 }
48 });
49
50 NotificationSpec {
51 route: Route::Both,
52 payload,
53 priority: Priority::Low,
54 }
55}
56
57pub fn parse_tsserver_event(payload: &Value) -> Option<DiagnosticsEvent> {
58 if payload.get("type")?.as_str()? != "event" {
59 return None;
60 }
61 let event_name = payload.get("event")?.as_str()?;
62 if event_name == REQUEST_COMPLETED {
63 let seq = payload
64 .get("body")
65 .and_then(|body| body.get("request_seq"))
66 .and_then(|value| value.as_u64())?;
67 return Some(DiagnosticsEvent::Completed { request_seq: seq });
68 }
69 let kind = DiagnosticsKind::from_event_name(event_name)?;
70
71 let body = payload.get("body")?;
72 let file = body.get("file")?.as_str()?;
73 let uri = file_path_to_uri(file)?;
74 let request_seq = body.get("request_seq").and_then(|value| value.as_u64());
75 let diagnostics = body
76 .get("diagnostics")
77 .and_then(|value| value.as_array())
78 .cloned()
79 .unwrap_or_default();
80
81 let lsp_diagnostics = diagnostics
82 .into_iter()
83 .filter_map(convert_diagnostic)
84 .collect::<Vec<_>>();
85
86 Some(DiagnosticsEvent::Report {
87 request_seq,
88 kind,
89 uri,
90 diagnostics: lsp_diagnostics,
91 })
92}
93
94fn convert_diagnostic(value: Value) -> Option<Diagnostic> {
95 let range = tsserver_range_from_value_lsp(&value)?;
96 let message = value.get("text")?.as_str()?.to_string();
97 let severity = map_severity(value.get("category").and_then(|v| v.as_str()));
98 let code = value
99 .get("code")
100 .and_then(|c| c.as_i64())
101 .map(|code| NumberOrString::Number(code as i32));
102
103 Some(Diagnostic {
104 range,
105 severity,
106 code,
107 source: Some("tsserver".to_string()),
108 message,
109 ..Diagnostic::default()
110 })
111}
112
113fn map_severity(category: Option<&str>) -> Option<DiagnosticSeverity> {
114 match category {
115 Some("error") => Some(DiagnosticSeverity::ERROR),
116 Some("warning") => Some(DiagnosticSeverity::WARNING),
117 Some("suggestion") => Some(DiagnosticSeverity::HINT),
118 Some("message") => Some(DiagnosticSeverity::INFORMATION),
119 _ => Some(DiagnosticSeverity::WARNING),
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn request_for_file_targets_both_servers() {
129 let spec = request_for_file("/workspace/foo.ts");
130 assert_eq!(spec.route, Route::Both);
131 let files = spec
132 .payload
133 .get("arguments")
134 .and_then(|args| args.get("files"))
135 .and_then(|entry| entry.as_array())
136 .expect("files not present");
137 assert_eq!(files, &[json!("/workspace/foo.ts")]);
138 }
139
140 #[test]
141 fn parse_tsserver_event_converts_diagnostics() {
142 let payload = json!({
143 "type": "event",
144 "event": "semanticDiag",
145 "body": {
146 "file": "/workspace/foo.ts",
147 "request_seq": 7,
148 "diagnostics": [{
149 "start": { "line": 1, "offset": 1 },
150 "end": { "line": 1, "offset": 4 },
151 "text": "oops",
152 "category": "error",
153 "code": 123,
154 }]
155 }
156 });
157
158 match parse_tsserver_event(&payload) {
159 Some(DiagnosticsEvent::Report {
160 uri,
161 diagnostics,
162 request_seq,
163 kind,
164 }) => {
165 assert_eq!(kind, DiagnosticsKind::Semantic);
166 assert_eq!(request_seq, Some(7));
167 assert_eq!(uri.to_string(), "file:///workspace/foo.ts");
168 assert_eq!(diagnostics.len(), 1);
169 assert_eq!(diagnostics[0].message, "oops");
170 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
171 }
172 other => panic!("unexpected diagnostics event: {other:?}"),
173 }
174 }
175
176 #[test]
177 fn parse_tsserver_event_detects_completion_events() {
178 let payload = json!({
179 "type": "event",
180 "event": "requestCompleted",
181 "body": { "request_seq": 99 }
182 });
183
184 match parse_tsserver_event(&payload) {
185 Some(DiagnosticsEvent::Completed { request_seq }) => assert_eq!(request_seq, 99),
186 other => panic!("expected completion event, got {other:?}"),
187 }
188 }
189}