1use std::collections::HashMap;
4
5use adam_lang::format_source;
6use lsp_server::{Connection, Message, Notification as ServerNotification, Request, Response};
7use lsp_types::{
8 DidChangeTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams, OneOf,
9 Position, PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
10 TextDocumentSyncKind, TextEdit, Uri,
11 notification::{
12 DidChangeTextDocument, DidOpenTextDocument, Notification as _, PublishDiagnostics,
13 },
14 request::{Formatting, Request as _},
15};
16
17use crate::diagnostics::diagnostics_for_source;
18
19const METHOD_NOT_FOUND: i32 = -32601;
21const INVALID_PARAMS: i32 = -32602;
24
25pub fn run() -> anyhow::Result<()> {
32 let (connection, io_threads) = Connection::stdio();
33 serve(&connection)?;
34 io_threads.join()?;
35 Ok(())
36}
37
38pub fn serve(connection: &Connection) -> anyhow::Result<()> {
49 let capabilities = serde_json::to_value(ServerCapabilities {
50 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
51 document_formatting_provider: Some(OneOf::Left(true)),
52 ..Default::default()
53 })?;
54 connection.initialize(capabilities)?;
55 main_loop(connection)
56}
57
58#[allow(clippy::mutable_key_type)]
67fn main_loop(connection: &Connection) -> anyhow::Result<()> {
68 let mut documents: HashMap<Uri, String> = HashMap::new();
69 for msg in &connection.receiver {
70 match msg {
71 Message::Request(req) => {
72 if connection.handle_shutdown(&req)? {
73 return Ok(());
74 }
75 handle_request(connection, &documents, req)?;
76 }
77 Message::Notification(not) => handle_notification(connection, &mut documents, not)?,
78 Message::Response(_) => {}
79 }
80 }
81 Ok(())
82}
83
84#[allow(clippy::mutable_key_type)]
93fn handle_request(
94 connection: &Connection,
95 documents: &HashMap<Uri, String>,
96 req: Request,
97) -> anyhow::Result<()> {
98 match req.method.as_str() {
99 Formatting::METHOD => {
100 let id = req.id.clone();
101 match req.extract::<DocumentFormattingParams>(Formatting::METHOD) {
102 Ok((id, params)) => {
103 let edits = documents
107 .get(¶ms.text_document.uri)
108 .map(|source| format_edits(source))
109 .unwrap_or_default();
110 connection
111 .sender
112 .send(Message::Response(Response::new_ok(id, edits)))?;
113 }
114 Err(error) => {
115 connection.sender.send(Message::Response(Response::new_err(
116 id,
117 INVALID_PARAMS,
118 error.to_string(),
119 )))?;
120 }
121 }
122 }
123 _ => {
124 let response = Response::new_err(
125 req.id.clone(),
126 METHOD_NOT_FOUND,
127 format!("unhandled method: {}", req.method),
128 );
129 connection.sender.send(Message::Response(response))?;
130 }
131 }
132 Ok(())
133}
134
135#[allow(clippy::mutable_key_type)]
147fn handle_notification(
148 connection: &Connection,
149 documents: &mut HashMap<Uri, String>,
150 not: ServerNotification,
151) -> anyhow::Result<()> {
152 match not.method.as_str() {
153 DidOpenTextDocument::METHOD => {
154 let params: DidOpenTextDocumentParams = match not.extract(DidOpenTextDocument::METHOD) {
155 Ok(params) => params,
156 Err(error) => {
157 eprintln!(
158 "adam-lsp: ignoring malformed {}: {error}",
159 DidOpenTextDocument::METHOD
160 );
161 return Ok(());
162 }
163 };
164 documents.insert(
165 params.text_document.uri.clone(),
166 params.text_document.text.clone(),
167 );
168 publish(
169 connection,
170 ¶ms.text_document.uri,
171 ¶ms.text_document.text,
172 )?;
173 }
174 DidChangeTextDocument::METHOD => {
175 let params: DidChangeTextDocumentParams =
176 match not.extract(DidChangeTextDocument::METHOD) {
177 Ok(params) => params,
178 Err(error) => {
179 eprintln!(
180 "adam-lsp: ignoring malformed {}: {error}",
181 DidChangeTextDocument::METHOD
182 );
183 return Ok(());
184 }
185 };
186 if let Some(change) = params.content_changes.into_iter().last() {
187 documents.insert(params.text_document.uri.clone(), change.text.clone());
188 publish(connection, ¶ms.text_document.uri, &change.text)?;
189 }
190 }
191 _ => {}
192 }
193 Ok(())
194}
195
196fn publish(connection: &Connection, uri: &Uri, source: &str) -> anyhow::Result<()> {
203 let params = PublishDiagnosticsParams {
204 uri: uri.clone(),
205 diagnostics: diagnostics_for_source(source),
206 version: None,
207 };
208 let notification = ServerNotification::new(PublishDiagnostics::METHOD.to_string(), params);
209 connection
210 .sender
211 .send(Message::Notification(notification))?;
212 Ok(())
213}
214
215fn format_edits(source: &str) -> Vec<TextEdit> {
224 match format_source(source) {
225 Ok(formatted) => vec![TextEdit {
226 range: whole_document_range(),
227 new_text: formatted,
228 }],
229 Err(_) => Vec::new(),
230 }
231}
232
233fn whole_document_range() -> Range {
238 Range {
239 start: Position {
240 line: 0,
241 character: 0,
242 },
243 end: Position {
244 line: u32::MAX,
245 character: u32::MAX,
246 },
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use lsp_server::{Connection, Message, Notification as ServerNotification, Request, RequestId};
253 use lsp_types::{
254 DidChangeTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
255 PublishDiagnosticsParams, TextDocumentContentChangeEvent, TextDocumentIdentifier,
256 TextDocumentItem, TextEdit, VersionedTextDocumentIdentifier,
257 notification::{
258 DidChangeTextDocument, DidOpenTextDocument, Notification as _, PublishDiagnostics,
259 },
260 request::{Formatting, Request as _},
261 };
262
263 use super::{format_edits, serve};
264
265 #[test]
266 fn format_edits_is_empty_for_a_syntax_error() {
267 assert!(format_edits("not a sheet at all").is_empty());
268 }
269
270 #[test]
271 fn format_edits_is_empty_for_a_recovered_syntax_error() {
272 assert!(format_edits("sheet s { cell x unknown_syntax }").is_empty());
273 }
274
275 #[test]
276 fn format_edits_returns_one_edit_replacing_the_whole_document() {
277 let edits = format_edits("sheet s{cell x:i32=1;}");
278 assert_eq!(edits.len(), 1);
279 assert_eq!(edits[0].new_text, "sheet s {\n cell x: i32 = 1;\n}\n");
280 }
281
282 #[test]
283 fn format_edits_formats_a_cell_with_a_filter() {
284 let edits = format_edits("sheet s { cell a:i32=1 filter _; }");
285 assert_eq!(edits.len(), 1);
286 assert_eq!(
287 edits[0].new_text,
288 "sheet s {\n cell a: i32 = 1 filter _;\n}\n"
289 );
290 }
291
292 #[test]
293 fn format_edits_formats_a_cell_with_a_range_inclusive_filter() {
294 let edits = format_edits("sheet s { cell a:i32=0 filter 0..=200; }");
300 assert_eq!(edits.len(), 1);
301 assert_eq!(
302 edits[0].new_text,
303 "sheet s {\n cell a: i32 = 0 filter 0..=200;\n}\n"
304 );
305 }
306
307 #[test]
308 fn formatting_request_returns_the_edit_for_a_previously_opened_document() {
309 let (server, client) = Connection::memory();
310 let server_thread = std::thread::spawn(move || serve(&server));
311 initialize(&client);
312
313 let uri: lsp_types::Uri = "file:///test.adm2".parse().unwrap();
314 client
315 .sender
316 .send(Message::Notification(ServerNotification::new(
317 DidOpenTextDocument::METHOD.to_string(),
318 DidOpenTextDocumentParams {
319 text_document: TextDocumentItem {
320 uri: uri.clone(),
321 language_id: "adam-lang".to_string(),
322 version: 1,
323 text: "sheet s { cell x: i32 = 1; }".to_string(),
324 },
325 },
326 )))
327 .unwrap();
328 expect_published(&client); client
331 .sender
332 .send(Message::Request(Request::new(
333 RequestId::from(3),
334 Formatting::METHOD.to_string(),
335 DocumentFormattingParams {
336 text_document: TextDocumentIdentifier { uri: uri.clone() },
337 options: lsp_types::FormattingOptions {
338 tab_size: 4,
339 insert_spaces: true,
340 ..Default::default()
341 },
342 work_done_progress_params: Default::default(),
343 },
344 )))
345 .unwrap();
346 let response = match client.receiver.recv().unwrap() {
347 Message::Response(r) => r,
348 other => panic!("expected a response, got {other:?}"),
349 };
350 let edits: Vec<TextEdit> =
351 serde_json::from_value(response.response_result.unwrap()).unwrap();
352 assert_eq!(edits.len(), 1);
353 assert_eq!(edits[0].new_text, "sheet s {\n cell x: i32 = 1;\n}\n");
354
355 shut_down(&client);
356 server_thread.join().unwrap().unwrap();
357 }
358
359 fn initialize(client: &Connection) {
361 client
362 .sender
363 .send(Message::Request(Request::new(
364 RequestId::from(1),
365 "initialize".to_string(),
366 serde_json::json!({}),
367 )))
368 .unwrap();
369 client.receiver.recv().unwrap();
370 client
371 .sender
372 .send(Message::Notification(ServerNotification::new(
373 "initialized".to_string(),
374 serde_json::json!({}),
375 )))
376 .unwrap();
377 }
378
379 fn shut_down(client: &Connection) {
381 client
382 .sender
383 .send(Message::Request(Request::new(
384 RequestId::from(2),
385 "shutdown".to_string(),
386 serde_json::json!(null),
387 )))
388 .unwrap();
389 client.receiver.recv().unwrap();
390 client
391 .sender
392 .send(Message::Notification(ServerNotification::new(
393 "exit".to_string(),
394 serde_json::json!(null),
395 )))
396 .unwrap();
397 }
398
399 fn expect_published(client: &Connection) -> PublishDiagnosticsParams {
402 let published = match client.receiver.recv().unwrap() {
403 Message::Notification(n) => n,
404 other => panic!("expected a notification, got {other:?}"),
405 };
406 assert_eq!(published.method, PublishDiagnostics::METHOD);
407 serde_json::from_value(published.params).unwrap()
408 }
409
410 #[test]
411 fn open_notification_triggers_a_publish_diagnostics_notification() {
412 let (server, client) = Connection::memory();
413 let server_thread = std::thread::spawn(move || serve(&server));
414 initialize(&client);
415
416 let uri: lsp_types::Uri = "file:///test.pm".parse().unwrap();
417 client
418 .sender
419 .send(Message::Notification(ServerNotification::new(
420 DidOpenTextDocument::METHOD.to_string(),
421 DidOpenTextDocumentParams {
422 text_document: TextDocumentItem {
423 uri: uri.clone(),
424 language_id: "adam-lang".to_string(),
425 version: 1,
426 text: "sheet s { cell x: i32 = 1.0; }".to_string(),
427 },
428 },
429 )))
430 .unwrap();
431
432 let params = expect_published(&client);
433 assert_eq!(params.uri, uri);
434 assert_eq!(params.diagnostics.len(), 1);
435
436 shut_down(&client);
437 server_thread.join().unwrap().unwrap();
438 }
439
440 #[test]
441 fn malformed_open_notification_is_skipped_not_crashed() {
442 let (server, client) = Connection::memory();
443 let server_thread = std::thread::spawn(move || serve(&server));
444 initialize(&client);
445
446 client
448 .sender
449 .send(Message::Notification(ServerNotification::new(
450 DidOpenTextDocument::METHOD.to_string(),
451 serde_json::json!({"not": "valid"}),
452 )))
453 .unwrap();
454
455 let uri: lsp_types::Uri = "file:///test.pm".parse().unwrap();
458 client
459 .sender
460 .send(Message::Notification(ServerNotification::new(
461 DidOpenTextDocument::METHOD.to_string(),
462 DidOpenTextDocumentParams {
463 text_document: TextDocumentItem {
464 uri: uri.clone(),
465 language_id: "adam-lang".to_string(),
466 version: 1,
467 text: "sheet s { cell x: i32 = 1; }".to_string(),
468 },
469 },
470 )))
471 .unwrap();
472
473 let params = expect_published(&client);
474 assert_eq!(params.uri, uri);
475 assert!(params.diagnostics.is_empty());
476
477 shut_down(&client);
478 server_thread.join().unwrap().unwrap();
479 }
480
481 #[test]
482 fn change_notification_after_open_triggers_a_second_publish_diagnostics_notification() {
483 let (server, client) = Connection::memory();
484 let server_thread = std::thread::spawn(move || serve(&server));
485 initialize(&client);
486
487 let uri: lsp_types::Uri = "file:///test.pm".parse().unwrap();
488 client
489 .sender
490 .send(Message::Notification(ServerNotification::new(
491 DidOpenTextDocument::METHOD.to_string(),
492 DidOpenTextDocumentParams {
493 text_document: TextDocumentItem {
494 uri: uri.clone(),
495 language_id: "adam-lang".to_string(),
496 version: 1,
497 text: "sheet s { cell x: i32 = 1; }".to_string(),
498 },
499 },
500 )))
501 .unwrap();
502 let first = expect_published(&client);
503 assert!(first.diagnostics.is_empty());
504
505 client
506 .sender
507 .send(Message::Notification(ServerNotification::new(
508 DidChangeTextDocument::METHOD.to_string(),
509 DidChangeTextDocumentParams {
510 text_document: VersionedTextDocumentIdentifier {
511 uri: uri.clone(),
512 version: 2,
513 },
514 content_changes: vec![TextDocumentContentChangeEvent {
515 range: None,
516 range_length: None,
517 text: "sheet s { cell x: i32 = 1.0; }".to_string(),
518 }],
519 },
520 )))
521 .unwrap();
522 let second = expect_published(&client);
523 assert_eq!(second.uri, uri);
524 assert_eq!(second.diagnostics.len(), 1);
525
526 shut_down(&client);
527 server_thread.join().unwrap().unwrap();
528 }
529
530 #[test]
531 fn unrecognized_request_method_gets_a_method_not_found_response() {
532 let (server, client) = Connection::memory();
533 let server_thread = std::thread::spawn(move || serve(&server));
534 initialize(&client);
535
536 client
537 .sender
538 .send(Message::Request(Request::new(
539 RequestId::from(3),
540 "textDocument/hover".to_string(),
541 serde_json::json!({}),
542 )))
543 .unwrap();
544
545 let response = match client.receiver.recv().unwrap() {
546 Message::Response(r) => r,
547 other => panic!("expected a response, got {other:?}"),
548 };
549 assert!(response.response_result.is_err());
550
551 shut_down(&client);
552 server_thread.join().unwrap().unwrap();
553 }
554}