1#![forbid(unsafe_code)]
107#![warn(missing_docs)]
108
109use serde::{Deserialize, Serialize};
110use serde_json::{Value as JsonValue, json};
111use std::collections::HashMap;
112
113pub mod diagnostics;
114pub mod format;
115pub mod hover;
116
117#[derive(Debug, Deserialize)]
119pub struct Request {
120 pub jsonrpc: String,
122 pub method: String,
124 #[serde(default)]
126 pub params: JsonValue,
127 pub id: Option<JsonValue>,
129}
130
131#[derive(Debug, Serialize)]
133pub struct Response {
134 pub jsonrpc: &'static str,
136 pub result: JsonValue,
138 pub id: JsonValue,
140}
141
142#[derive(Debug, Serialize)]
144pub struct ErrorResponse {
145 pub jsonrpc: &'static str,
147 pub error: ErrorObject,
149 pub id: JsonValue,
151}
152
153#[derive(Debug, Serialize)]
155pub struct ErrorObject {
156 pub code: i32,
158 pub message: String,
160}
161
162#[derive(Debug, Serialize)]
165pub struct Notification {
166 pub jsonrpc: &'static str,
168 pub method: &'static str,
170 pub params: JsonValue,
172}
173
174#[derive(Debug, Default)]
178pub struct HandleOutcome {
179 pub reply: Option<String>,
181 pub notifications: Vec<String>,
184}
185
186impl HandleOutcome {
187 pub fn reply(payload: String) -> Self {
198 HandleOutcome {
199 reply: Some(payload),
200 notifications: Vec::new(),
201 }
202 }
203
204 pub fn notify(payload: String) -> Self {
215 HandleOutcome {
216 reply: None,
217 notifications: vec![payload],
218 }
219 }
220
221 pub fn silent() -> Self {
232 HandleOutcome::default()
233 }
234}
235
236#[derive(Debug, Default)]
247pub struct Server {
248 documents: HashMap<String, String>,
250 initialized: bool,
252 shutting_down: bool,
255}
256
257impl Server {
258 #[must_use]
268 pub fn new() -> Self {
269 Server::default()
270 }
271
272 #[must_use]
282 pub fn open_document_count(&self) -> usize {
283 self.documents.len()
284 }
285
286 #[must_use]
297 pub fn document(&self, uri: &str) -> Option<&str> {
298 self.documents.get(uri).map(String::as_str)
299 }
300
301 pub fn handle_message(&mut self, raw: &str) -> HandleOutcome {
315 let req: Request = match serde_json::from_str(raw) {
316 Ok(r) => r,
317 Err(e) => {
318 return HandleOutcome::reply(error_str(
319 JsonValue::Null,
320 -32700,
321 format!("parse error: {e}"),
322 ));
323 }
324 };
325 if req.jsonrpc != "2.0" {
326 return HandleOutcome::reply(error_str(
327 req.id.unwrap_or(JsonValue::Null),
328 -32600,
329 "invalid request: jsonrpc must be \"2.0\"".into(),
330 ));
331 }
332 let id = req.id.clone();
333 let result = self.dispatch(&req.method, req.params);
334
335 let mut outcome = HandleOutcome::default();
336 match (id, result) {
337 (None, Ok(side)) => {
338 outcome.notifications = side.notifications;
339 }
340 (None, Err(_)) => {
341 }
343 (Some(id), Ok(side)) => {
344 outcome.reply = Some(
345 serde_json::to_string(&Response {
346 jsonrpc: "2.0",
347 result: side.value,
348 id,
349 })
350 .expect("infallible serialise"),
351 );
352 outcome.notifications = side.notifications;
353 }
354 (Some(id), Err((code, msg))) => {
355 outcome.reply = Some(error_str(id, code, msg));
356 }
357 }
358 outcome
359 }
360
361 fn dispatch(&mut self, method: &str, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
362 if self.shutting_down && method != "exit" {
363 return Err((-32600, format!("server is shutting down; refused {method}")));
364 }
365 match method {
366 "initialize" => {
367 self.initialized = true;
368 Ok(DispatchOk::value(json!({
369 "capabilities": {
370 "textDocumentSync": 1,
371 "documentFormattingProvider": true,
372 "hoverProvider": true,
373 },
374 "serverInfo": {
375 "name": "noyalib-lsp",
376 "version": env!("CARGO_PKG_VERSION"),
377 }
378 })))
379 }
380 "initialized" => Ok(DispatchOk::value(JsonValue::Null)),
381 "shutdown" => {
382 self.shutting_down = true;
383 Ok(DispatchOk::value(JsonValue::Null))
384 }
385 "exit" => Ok(DispatchOk::value(JsonValue::Null)),
386 "textDocument/didOpen" => self.did_open(params),
387 "textDocument/didChange" => self.did_change(params),
388 "textDocument/didClose" => self.did_close(params),
389 "textDocument/formatting" => self.formatting(params),
390 "textDocument/hover" => self.hover(params),
391 other => Err((-32601, format!("method not found: {other}"))),
392 }
393 }
394
395 fn did_open(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
396 let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
397 let text = params
398 .pointer("/textDocument/text")
399 .and_then(|v| v.as_str())
400 .unwrap_or_default()
401 .to_owned();
402 let _ = self.documents.insert(uri.clone(), text.clone());
403 let mut ok = DispatchOk::value(JsonValue::Null);
404 if let Some(note) = diagnostics::publish_diagnostics(&uri, &text) {
405 ok.notifications.push(note);
406 }
407 Ok(ok)
408 }
409
410 fn did_change(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
411 let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
412 let text = params
415 .pointer("/contentChanges/0/text")
416 .and_then(|v| v.as_str())
417 .ok_or((-32602, "missing contentChanges[0].text".into()))?
418 .to_owned();
419 let _ = self.documents.insert(uri.clone(), text.clone());
420 let mut ok = DispatchOk::value(JsonValue::Null);
421 if let Some(note) = diagnostics::publish_diagnostics(&uri, &text) {
422 ok.notifications.push(note);
423 }
424 Ok(ok)
425 }
426
427 fn did_close(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
428 let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
429 let _ = self.documents.remove(&uri);
430 Ok(DispatchOk::value(JsonValue::Null))
431 }
432
433 fn formatting(&self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
434 let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
435 let text = self
436 .documents
437 .get(&uri)
438 .ok_or((-32602, format!("document not open: {uri}")))?;
439 let edits = format::full_document_edits(text)
440 .map_err(|e| (-32603, format!("format failed: {e}")))?;
441 Ok(DispatchOk::value(serde_json::to_value(edits).unwrap()))
442 }
443
444 fn hover(&self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
445 let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
446 let line = params
447 .pointer("/position/line")
448 .and_then(|v| v.as_u64())
449 .ok_or((-32602, "missing position.line".into()))? as usize;
450 let column = params
451 .pointer("/position/character")
452 .and_then(|v| v.as_u64())
453 .ok_or((-32602, "missing position.character".into()))? as usize;
454 let text = self
455 .documents
456 .get(&uri)
457 .ok_or((-32602, format!("document not open: {uri}")))?;
458 Ok(DispatchOk::value(hover::hover_at(text, line, column)))
459 }
460}
461
462struct DispatchOk {
463 value: JsonValue,
464 notifications: Vec<String>,
465}
466
467impl DispatchOk {
468 fn value(v: JsonValue) -> Self {
469 DispatchOk {
470 value: v,
471 notifications: Vec::new(),
472 }
473 }
474}
475
476fn uri_from_params(params: &JsonValue) -> Option<String> {
477 params
478 .pointer("/textDocument/uri")
479 .and_then(|v| v.as_str())
480 .map(str::to_owned)
481}
482
483pub fn error_str(id: JsonValue, code: i32, message: String) -> String {
494 serde_json::to_string(&ErrorResponse {
495 jsonrpc: "2.0",
496 error: ErrorObject { code, message },
497 id,
498 })
499 .expect("infallible serialise")
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 fn parse_reply(out: &HandleOutcome) -> JsonValue {
507 let s = out.reply.as_deref().expect("expected reply");
508 serde_json::from_str(s).unwrap()
509 }
510
511 #[test]
512 fn handle_message_returns_parse_error_on_bad_json() {
513 let mut s = Server::new();
514 let out = s.handle_message("{not json");
515 let v = parse_reply(&out);
516 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32700);
517 assert!(v["id"].is_null());
518 }
519
520 #[test]
521 fn handle_message_rejects_non_2_0_jsonrpc() {
522 let mut s = Server::new();
523 let req = json!({"jsonrpc": "1.0", "method": "initialize", "id": 1});
524 let out = s.handle_message(&req.to_string());
525 let v = parse_reply(&out);
526 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32600);
527 }
528
529 #[test]
530 fn initialize_returns_capabilities_and_server_info() {
531 let mut s = Server::new();
532 let req = json!({"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}});
533 let out = s.handle_message(&req.to_string());
534 let v = parse_reply(&out);
535 assert_eq!(
536 v["result"]["serverInfo"]["name"].as_str(),
537 Some("noyalib-lsp")
538 );
539 assert_eq!(
540 v["result"]["capabilities"]["documentFormattingProvider"].as_bool(),
541 Some(true),
542 );
543 assert_eq!(
544 v["result"]["capabilities"]["hoverProvider"].as_bool(),
545 Some(true),
546 );
547 assert_eq!(
548 v["result"]["capabilities"]["textDocumentSync"].as_i64(),
549 Some(1),
550 );
551 }
552
553 #[test]
554 fn unknown_method_returns_method_not_found() {
555 let mut s = Server::new();
556 let req = json!({"jsonrpc": "2.0", "method": "frobnicate", "id": 7});
557 let out = s.handle_message(&req.to_string());
558 let v = parse_reply(&out);
559 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32601);
560 }
561
562 #[test]
563 fn shutdown_then_non_exit_method_is_rejected() {
564 let mut s = Server::new();
565 let _ =
566 s.handle_message(&json!({"jsonrpc": "2.0", "method": "shutdown", "id": 1}).to_string());
567 let out = s.handle_message(
568 &json!({"jsonrpc": "2.0", "method": "textDocument/hover", "id": 2,
569 "params": {"textDocument": {"uri": "f"}, "position": {"line": 0, "character": 0}}})
570 .to_string(),
571 );
572 let v = parse_reply(&out);
573 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32600);
574 }
575
576 #[test]
577 fn exit_after_shutdown_succeeds() {
578 let mut s = Server::new();
579 let _ =
580 s.handle_message(&json!({"jsonrpc": "2.0", "method": "shutdown", "id": 1}).to_string());
581 let out = s.handle_message(&json!({"jsonrpc": "2.0", "method": "exit"}).to_string());
582 assert!(out.reply.is_none());
584 }
585
586 #[test]
587 fn did_open_records_document_and_publishes_diagnostics() {
588 let mut s = Server::new();
589 let req = json!({
590 "jsonrpc": "2.0",
591 "method": "textDocument/didOpen",
592 "params": {
593 "textDocument": {
594 "uri": "file:///tmp/a.yaml",
595 "languageId": "yaml",
596 "version": 1,
597 "text": "name: noyalib\n"
598 }
599 }
600 });
601 let out = s.handle_message(&req.to_string());
602 assert!(out.reply.is_none());
604 assert_eq!(out.notifications.len(), 1);
606 let note: JsonValue = serde_json::from_str(&out.notifications[0]).unwrap();
607 assert_eq!(
608 note["method"].as_str(),
609 Some("textDocument/publishDiagnostics"),
610 );
611 assert_eq!(s.open_document_count(), 1);
612 assert_eq!(s.document("file:///tmp/a.yaml"), Some("name: noyalib\n"));
613 }
614
615 #[test]
616 fn did_change_overwrites_text_and_re_publishes() {
617 let mut s = Server::new();
618 let _ = s.handle_message(
619 &json!({
620 "jsonrpc": "2.0",
621 "method": "textDocument/didOpen",
622 "params": {
623 "textDocument": {
624 "uri": "file:///tmp/b.yaml", "languageId": "yaml",
625 "version": 1, "text": "a: 1\n"
626 }
627 }
628 })
629 .to_string(),
630 );
631 let out = s.handle_message(
632 &json!({
633 "jsonrpc": "2.0",
634 "method": "textDocument/didChange",
635 "params": {
636 "textDocument": {"uri": "file:///tmp/b.yaml", "version": 2},
637 "contentChanges": [{"text": "a: 2\n"}]
638 }
639 })
640 .to_string(),
641 );
642 assert_eq!(out.notifications.len(), 1);
643 assert_eq!(s.document("file:///tmp/b.yaml"), Some("a: 2\n"));
644 }
645
646 #[test]
647 fn did_close_drops_document() {
648 let mut s = Server::new();
649 let _ = s.handle_message(
650 &json!({
651 "jsonrpc": "2.0",
652 "method": "textDocument/didOpen",
653 "params": {"textDocument": {
654 "uri": "f", "languageId": "yaml", "version": 1, "text": "x: 1\n"
655 }}
656 })
657 .to_string(),
658 );
659 let _ = s.handle_message(
660 &json!({
661 "jsonrpc": "2.0",
662 "method": "textDocument/didClose",
663 "params": {"textDocument": {"uri": "f"}}
664 })
665 .to_string(),
666 );
667 assert_eq!(s.open_document_count(), 0);
668 }
669
670 #[test]
671 fn formatting_returns_text_edits() {
672 let mut s = Server::new();
673 let _ = s.handle_message(
674 &json!({
675 "jsonrpc": "2.0",
676 "method": "textDocument/didOpen",
677 "params": {"textDocument": {
678 "uri": "f", "languageId": "yaml", "version": 1, "text": "a: 1\n"
679 }}
680 })
681 .to_string(),
682 );
683 let out = s.handle_message(
684 &json!({
685 "jsonrpc": "2.0",
686 "method": "textDocument/formatting",
687 "id": 5,
688 "params": {"textDocument": {"uri": "f"}, "options": {"tabSize": 2, "insertSpaces": true}}
689 })
690 .to_string(),
691 );
692 let v = parse_reply(&out);
693 assert!(v["result"].is_array());
694 }
695
696 #[test]
697 fn formatting_unknown_uri_errors() {
698 let mut s = Server::new();
699 let out = s.handle_message(
700 &json!({
701 "jsonrpc": "2.0",
702 "method": "textDocument/formatting",
703 "id": 6,
704 "params": {"textDocument": {"uri": "missing"}, "options": {}}
705 })
706 .to_string(),
707 );
708 let v = parse_reply(&out);
709 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32602);
710 }
711
712 #[test]
713 fn hover_unknown_uri_errors() {
714 let mut s = Server::new();
715 let out = s.handle_message(
716 &json!({
717 "jsonrpc": "2.0",
718 "method": "textDocument/hover",
719 "id": 7,
720 "params": {
721 "textDocument": {"uri": "missing"},
722 "position": {"line": 0, "character": 0}
723 }
724 })
725 .to_string(),
726 );
727 let v = parse_reply(&out);
728 assert_eq!(v["error"]["code"].as_i64().unwrap(), -32602);
729 }
730
731 #[test]
732 fn error_str_renders_canonical_envelope() {
733 let s = error_str(json!(42), -32000, "boom".into());
734 let v: JsonValue = serde_json::from_str(&s).unwrap();
735 assert_eq!(v["jsonrpc"].as_str(), Some("2.0"));
736 assert_eq!(v["id"].as_i64(), Some(42));
737 assert_eq!(v["error"]["code"].as_i64(), Some(-32000));
738 }
739
740 #[test]
741 fn handle_outcome_helpers_construct_correctly() {
742 let r = HandleOutcome::reply("hi".into());
743 assert!(r.reply.is_some());
744 assert!(r.notifications.is_empty());
745 let n = HandleOutcome::notify("x".into());
746 assert!(n.reply.is_none());
747 assert_eq!(n.notifications.len(), 1);
748 let s = HandleOutcome::silent();
749 assert!(s.reply.is_none());
750 assert!(s.notifications.is_empty());
751 }
752}