use std::collections::HashMap;
use std::io::{BufRead, Write};
use crate::{Severity, analyse};
use oxml_json::{self as json, Json};
pub fn serve<R: BufRead, W: Write>(input: R, mut output: W) {
let mut server = Server::new();
let mut reader = input;
while let Some(message) = read_message(&mut reader) {
let Ok(request) = json::parse(&message) else {
let _ = write_message(
&mut output,
&error_response(&Json::Null, -32700, "parse error"),
);
continue;
};
for reply in server.handle(&request) {
if write_message(&mut output, &reply).is_err() {
return;
}
}
if server.exited {
return;
}
}
}
struct Server {
documents: HashMap<String, String>,
exited: bool,
shutting_down: bool,
}
impl Server {
fn new() -> Self {
Self {
documents: HashMap::new(),
exited: false,
shutting_down: false,
}
}
fn handle(&mut self, request: &Json) -> Vec<Json> {
let method = request.get("method").and_then(Json::as_str).unwrap_or("");
let id = request.get("id").cloned().unwrap_or(Json::Null);
let params = request.get("params");
match method {
"initialize" => vec![ok_response(&id, initialize_result())],
"initialized" => Vec::new(),
"shutdown" => {
self.shutting_down = true;
vec![ok_response(&id, Json::Null)]
}
"exit" => {
self.exited = true;
Vec::new()
}
"textDocument/didOpen" => {
let Some((uri, text)) = document_from_open(params) else {
return Vec::new();
};
let _ = self.documents.insert(uri.clone(), text.clone());
vec![diagnostics_notification(&uri, &text)]
}
"textDocument/didChange" => {
let Some((uri, text)) = document_from_change(params) else {
return Vec::new();
};
let _ = self.documents.insert(uri.clone(), text.clone());
vec![diagnostics_notification(&uri, &text)]
}
"textDocument/didClose" => {
if let Some(uri) = uri_of(params) {
let _ = self.documents.remove(&uri);
return vec![publish(&uri, Vec::new())];
}
Vec::new()
}
_ if matches!(id, Json::Null) => Vec::new(),
_ => vec![error_response(&id, -32601, "method not found")],
}
}
}
fn initialize_result() -> Json {
Json::object(vec![(
"capabilities",
Json::object(vec![
("textDocumentSync", Json::Number(1.0)),
]),
)])
}
fn diagnostics_notification(uri: &str, text: &str) -> Json {
let diagnostics = analyse(text)
.into_iter()
.map(|d| {
Json::object(vec![
(
"range",
Json::object(vec![
("start", position(d.start.line, d.start.character)),
("end", position(d.end.line, d.end.character)),
]),
),
(
"severity",
Json::Number(f64::from(severity_code(d.severity))),
),
("code", Json::str(d.code)),
("source", Json::str("oxml")),
("message", Json::str(d.message)),
])
})
.collect();
publish(uri, diagnostics)
}
fn publish(uri: &str, diagnostics: Vec<Json>) -> Json {
Json::object(vec![
("jsonrpc", Json::str("2.0")),
("method", Json::str("textDocument/publishDiagnostics")),
(
"params",
Json::object(vec![
("uri", Json::str(uri)),
("diagnostics", Json::Array(diagnostics)),
]),
),
])
}
fn position(line: usize, character: usize) -> Json {
#[allow(clippy::cast_precision_loss)]
Json::object(vec![
("line", Json::Number(line as f64)),
("character", Json::Number(character as f64)),
])
}
const fn severity_code(s: Severity) -> u8 {
match s {
Severity::Error => 1,
Severity::Warning => 2,
Severity::Information => 3,
Severity::Hint => 4,
}
}
fn uri_of(params: Option<&Json>) -> Option<String> {
Some(
params?
.get("textDocument")?
.get("uri")?
.as_str()?
.to_owned(),
)
}
fn document_from_open(params: Option<&Json>) -> Option<(String, String)> {
let doc = params?.get("textDocument")?;
Some((
doc.get("uri")?.as_str()?.to_owned(),
doc.get("text")?.as_str()?.to_owned(),
))
}
fn document_from_change(params: Option<&Json>) -> Option<(String, String)> {
let uri = uri_of(params)?;
let Json::Array(changes) = params?.get("contentChanges")? else {
return None;
};
let text = changes.last()?.get("text")?.as_str()?.to_owned();
Some((uri, text))
}
fn ok_response(id: &Json, result: Json) -> Json {
Json::object(vec![
("jsonrpc", Json::str("2.0")),
("id", id.clone()),
("result", result),
])
}
fn error_response(id: &Json, code: i32, message: &str) -> Json {
Json::object(vec![
("jsonrpc", Json::str("2.0")),
("id", id.clone()),
(
"error",
Json::object(vec![
("code", Json::Number(f64::from(code))),
("message", Json::str(message)),
]),
),
])
}
fn read_message<R: BufRead>(reader: &mut R) -> Option<String> {
let mut length: Option<usize> = None;
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => return None,
Ok(_) => {}
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some((name, value)) = trimmed.split_once(':') {
if name.trim().eq_ignore_ascii_case("content-length") {
length = value.trim().parse().ok();
}
}
}
let length = length?;
let mut body = vec![0_u8; length];
reader.read_exact(&mut body).ok()?;
String::from_utf8(body).ok()
}
fn write_message<W: Write>(out: &mut W, message: &Json) -> std::io::Result<()> {
let body = message.to_json();
write!(out, "Content-Length: {}\r\n\r\n{body}", body.len())?;
out.flush()
}