#![forbid(unsafe_code)]
#![warn(missing_docs)]
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use std::collections::HashMap;
pub mod diagnostics;
pub mod format;
pub mod hover;
#[derive(Debug, Deserialize)]
pub struct Request {
pub jsonrpc: String,
pub method: String,
#[serde(default)]
pub params: JsonValue,
pub id: Option<JsonValue>,
}
#[derive(Debug, Serialize)]
pub struct Response {
pub jsonrpc: &'static str,
pub result: JsonValue,
pub id: JsonValue,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub jsonrpc: &'static str,
pub error: ErrorObject,
pub id: JsonValue,
}
#[derive(Debug, Serialize)]
pub struct ErrorObject {
pub code: i32,
pub message: String,
}
#[derive(Debug, Serialize)]
pub struct Notification {
pub jsonrpc: &'static str,
pub method: &'static str,
pub params: JsonValue,
}
#[derive(Debug, Default)]
pub struct HandleOutcome {
pub reply: Option<String>,
pub notifications: Vec<String>,
}
impl HandleOutcome {
pub fn reply(payload: String) -> Self {
HandleOutcome {
reply: Some(payload),
notifications: Vec::new(),
}
}
pub fn notify(payload: String) -> Self {
HandleOutcome {
reply: None,
notifications: vec![payload],
}
}
pub fn silent() -> Self {
HandleOutcome::default()
}
}
#[derive(Debug, Default)]
pub struct Server {
documents: HashMap<String, String>,
initialized: bool,
shutting_down: bool,
}
impl Server {
#[must_use]
pub fn new() -> Self {
Server::default()
}
#[must_use]
pub fn open_document_count(&self) -> usize {
self.documents.len()
}
#[must_use]
pub fn document(&self, uri: &str) -> Option<&str> {
self.documents.get(uri).map(String::as_str)
}
pub fn handle_message(&mut self, raw: &str) -> HandleOutcome {
let req: Request = match serde_json::from_str(raw) {
Ok(r) => r,
Err(e) => {
return HandleOutcome::reply(error_str(
JsonValue::Null,
-32700,
format!("parse error: {e}"),
));
}
};
if req.jsonrpc != "2.0" {
return HandleOutcome::reply(error_str(
req.id.unwrap_or(JsonValue::Null),
-32600,
"invalid request: jsonrpc must be \"2.0\"".into(),
));
}
let id = req.id.clone();
let result = self.dispatch(&req.method, req.params);
let mut outcome = HandleOutcome::default();
match (id, result) {
(None, Ok(side)) => {
outcome.notifications = side.notifications;
}
(None, Err(_)) => {
}
(Some(id), Ok(side)) => {
outcome.reply = Some(
serde_json::to_string(&Response {
jsonrpc: "2.0",
result: side.value,
id,
})
.expect("infallible serialise"),
);
outcome.notifications = side.notifications;
}
(Some(id), Err((code, msg))) => {
outcome.reply = Some(error_str(id, code, msg));
}
}
outcome
}
fn dispatch(&mut self, method: &str, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
if self.shutting_down && method != "exit" {
return Err((-32600, format!("server is shutting down; refused {method}")));
}
match method {
"initialize" => {
self.initialized = true;
Ok(DispatchOk::value(json!({
"capabilities": {
"textDocumentSync": 1,
"documentFormattingProvider": true,
"hoverProvider": true,
},
"serverInfo": {
"name": "noyalib-lsp",
"version": env!("CARGO_PKG_VERSION"),
}
})))
}
"initialized" => Ok(DispatchOk::value(JsonValue::Null)),
"shutdown" => {
self.shutting_down = true;
Ok(DispatchOk::value(JsonValue::Null))
}
"exit" => Ok(DispatchOk::value(JsonValue::Null)),
"textDocument/didOpen" => self.did_open(params),
"textDocument/didChange" => self.did_change(params),
"textDocument/didClose" => self.did_close(params),
"textDocument/formatting" => self.formatting(params),
"textDocument/hover" => self.hover(params),
other => Err((-32601, format!("method not found: {other}"))),
}
}
fn did_open(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
let text = params
.pointer("/textDocument/text")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_owned();
let _ = self.documents.insert(uri.clone(), text.clone());
let mut ok = DispatchOk::value(JsonValue::Null);
if let Some(note) = diagnostics::publish_diagnostics(&uri, &text) {
ok.notifications.push(note);
}
Ok(ok)
}
fn did_change(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
let text = params
.pointer("/contentChanges/0/text")
.and_then(|v| v.as_str())
.ok_or((-32602, "missing contentChanges[0].text".into()))?
.to_owned();
let _ = self.documents.insert(uri.clone(), text.clone());
let mut ok = DispatchOk::value(JsonValue::Null);
if let Some(note) = diagnostics::publish_diagnostics(&uri, &text) {
ok.notifications.push(note);
}
Ok(ok)
}
fn did_close(&mut self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
let _ = self.documents.remove(&uri);
Ok(DispatchOk::value(JsonValue::Null))
}
fn formatting(&self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
let text = self
.documents
.get(&uri)
.ok_or((-32602, format!("document not open: {uri}")))?;
let edits = format::full_document_edits(text)
.map_err(|e| (-32603, format!("format failed: {e}")))?;
Ok(DispatchOk::value(serde_json::to_value(edits).unwrap()))
}
fn hover(&self, params: JsonValue) -> Result<DispatchOk, (i32, String)> {
let uri = uri_from_params(¶ms).ok_or((-32602, "missing textDocument.uri".into()))?;
let line = params
.pointer("/position/line")
.and_then(|v| v.as_u64())
.ok_or((-32602, "missing position.line".into()))? as usize;
let column = params
.pointer("/position/character")
.and_then(|v| v.as_u64())
.ok_or((-32602, "missing position.character".into()))? as usize;
let text = self
.documents
.get(&uri)
.ok_or((-32602, format!("document not open: {uri}")))?;
Ok(DispatchOk::value(hover::hover_at(text, line, column)))
}
}
struct DispatchOk {
value: JsonValue,
notifications: Vec<String>,
}
impl DispatchOk {
fn value(v: JsonValue) -> Self {
DispatchOk {
value: v,
notifications: Vec::new(),
}
}
}
fn uri_from_params(params: &JsonValue) -> Option<String> {
params
.pointer("/textDocument/uri")
.and_then(|v| v.as_str())
.map(str::to_owned)
}
pub fn error_str(id: JsonValue, code: i32, message: String) -> String {
serde_json::to_string(&ErrorResponse {
jsonrpc: "2.0",
error: ErrorObject { code, message },
id,
})
.expect("infallible serialise")
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_reply(out: &HandleOutcome) -> JsonValue {
let s = out.reply.as_deref().expect("expected reply");
serde_json::from_str(s).unwrap()
}
#[test]
fn handle_message_returns_parse_error_on_bad_json() {
let mut s = Server::new();
let out = s.handle_message("{not json");
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32700);
assert!(v["id"].is_null());
}
#[test]
fn handle_message_rejects_non_2_0_jsonrpc() {
let mut s = Server::new();
let req = json!({"jsonrpc": "1.0", "method": "initialize", "id": 1});
let out = s.handle_message(&req.to_string());
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32600);
}
#[test]
fn initialize_returns_capabilities_and_server_info() {
let mut s = Server::new();
let req = json!({"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}});
let out = s.handle_message(&req.to_string());
let v = parse_reply(&out);
assert_eq!(
v["result"]["serverInfo"]["name"].as_str(),
Some("noyalib-lsp")
);
assert_eq!(
v["result"]["capabilities"]["documentFormattingProvider"].as_bool(),
Some(true),
);
assert_eq!(
v["result"]["capabilities"]["hoverProvider"].as_bool(),
Some(true),
);
assert_eq!(
v["result"]["capabilities"]["textDocumentSync"].as_i64(),
Some(1),
);
}
#[test]
fn unknown_method_returns_method_not_found() {
let mut s = Server::new();
let req = json!({"jsonrpc": "2.0", "method": "frobnicate", "id": 7});
let out = s.handle_message(&req.to_string());
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32601);
}
#[test]
fn shutdown_then_non_exit_method_is_rejected() {
let mut s = Server::new();
let _ =
s.handle_message(&json!({"jsonrpc": "2.0", "method": "shutdown", "id": 1}).to_string());
let out = s.handle_message(
&json!({"jsonrpc": "2.0", "method": "textDocument/hover", "id": 2,
"params": {"textDocument": {"uri": "f"}, "position": {"line": 0, "character": 0}}})
.to_string(),
);
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32600);
}
#[test]
fn exit_after_shutdown_succeeds() {
let mut s = Server::new();
let _ =
s.handle_message(&json!({"jsonrpc": "2.0", "method": "shutdown", "id": 1}).to_string());
let out = s.handle_message(&json!({"jsonrpc": "2.0", "method": "exit"}).to_string());
assert!(out.reply.is_none());
}
#[test]
fn did_open_records_document_and_publishes_diagnostics() {
let mut s = Server::new();
let req = json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/a.yaml",
"languageId": "yaml",
"version": 1,
"text": "name: noyalib\n"
}
}
});
let out = s.handle_message(&req.to_string());
assert!(out.reply.is_none());
assert_eq!(out.notifications.len(), 1);
let note: JsonValue = serde_json::from_str(&out.notifications[0]).unwrap();
assert_eq!(
note["method"].as_str(),
Some("textDocument/publishDiagnostics"),
);
assert_eq!(s.open_document_count(), 1);
assert_eq!(s.document("file:///tmp/a.yaml"), Some("name: noyalib\n"));
}
#[test]
fn did_change_overwrites_text_and_re_publishes() {
let mut s = Server::new();
let _ = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///tmp/b.yaml", "languageId": "yaml",
"version": 1, "text": "a: 1\n"
}
}
})
.to_string(),
);
let out = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {"uri": "file:///tmp/b.yaml", "version": 2},
"contentChanges": [{"text": "a: 2\n"}]
}
})
.to_string(),
);
assert_eq!(out.notifications.len(), 1);
assert_eq!(s.document("file:///tmp/b.yaml"), Some("a: 2\n"));
}
#[test]
fn did_close_drops_document() {
let mut s = Server::new();
let _ = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {"textDocument": {
"uri": "f", "languageId": "yaml", "version": 1, "text": "x: 1\n"
}}
})
.to_string(),
);
let _ = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/didClose",
"params": {"textDocument": {"uri": "f"}}
})
.to_string(),
);
assert_eq!(s.open_document_count(), 0);
}
#[test]
fn formatting_returns_text_edits() {
let mut s = Server::new();
let _ = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {"textDocument": {
"uri": "f", "languageId": "yaml", "version": 1, "text": "a: 1\n"
}}
})
.to_string(),
);
let out = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/formatting",
"id": 5,
"params": {"textDocument": {"uri": "f"}, "options": {"tabSize": 2, "insertSpaces": true}}
})
.to_string(),
);
let v = parse_reply(&out);
assert!(v["result"].is_array());
}
#[test]
fn formatting_unknown_uri_errors() {
let mut s = Server::new();
let out = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/formatting",
"id": 6,
"params": {"textDocument": {"uri": "missing"}, "options": {}}
})
.to_string(),
);
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32602);
}
#[test]
fn hover_unknown_uri_errors() {
let mut s = Server::new();
let out = s.handle_message(
&json!({
"jsonrpc": "2.0",
"method": "textDocument/hover",
"id": 7,
"params": {
"textDocument": {"uri": "missing"},
"position": {"line": 0, "character": 0}
}
})
.to_string(),
);
let v = parse_reply(&out);
assert_eq!(v["error"]["code"].as_i64().unwrap(), -32602);
}
#[test]
fn error_str_renders_canonical_envelope() {
let s = error_str(json!(42), -32000, "boom".into());
let v: JsonValue = serde_json::from_str(&s).unwrap();
assert_eq!(v["jsonrpc"].as_str(), Some("2.0"));
assert_eq!(v["id"].as_i64(), Some(42));
assert_eq!(v["error"]["code"].as_i64(), Some(-32000));
}
#[test]
fn handle_outcome_helpers_construct_correctly() {
let r = HandleOutcome::reply("hi".into());
assert!(r.reply.is_some());
assert!(r.notifications.is_empty());
let n = HandleOutcome::notify("x".into());
assert!(n.reply.is_none());
assert_eq!(n.notifications.len(), 1);
let s = HandleOutcome::silent();
assert!(s.reply.is_none());
assert!(s.notifications.is_empty());
}
}