use std::io::{BufRead, Write};
use crate::{Severity, analyse};
use oxml_json::{self as json, Json};
mod positions;
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 {
exited: bool,
shutting_down: bool,
}
impl Server {
fn new() -> Self {
Self {
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 Some(id) = request_id(request) else {
return vec![error_response(
&Json::Null,
-32600,
"invalid request id",
)];
};
let params = request.get("params");
if self.shutting_down && method != "exit" {
return if request.get("id").is_some() {
vec![error_response(&id, -32600, "server is shutting down")]
} else {
Vec::new()
};
}
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()
}
_ => document_message(method, params, &id),
}
}
}
fn request_id(request: &Json) -> Option<Json> {
match request.get("id") {
None => Some(Json::Null),
Some(id @ (Json::String(_) | Json::Number(_) | Json::Null)) => {
Some(id.clone())
}
_ => None,
}
}
fn document_message(
method: &str,
params: Option<&Json>,
id: &Json,
) -> Vec<Json> {
match method {
"textDocument/didOpen" => {
let Some((uri, text)) = document_from_open(params) else {
return Vec::new();
};
vec![diagnostics_notification(&uri, &text)]
}
"textDocument/didChange" => {
let Some((uri, text)) = document_from_change(params) else {
return Vec::new();
};
vec![diagnostics_notification(&uri, &text)]
}
"textDocument/didClose" => {
if let Some(uri) = uri_of(params) {
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 mut diagnostics = analyse(text);
positions::utf16(text, &mut diagnostics);
let diagnostics = diagnostics
.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 length = read_header_length(reader);
let length = length.filter(|size| *size <= 16 * 1024 * 1024)?;
let mut body = vec![0_u8; length];
reader.read_exact(&mut body).ok()?;
String::from_utf8(body).ok()
}
fn read_header_length<R: BufRead>(reader: &mut R) -> Option<usize> {
let mut length: Option<usize> = None;
let mut remaining_header_bytes = 8192_usize;
loop {
let mut line = String::new();
let mut bounded = std::io::Read::take(
&mut *reader,
(remaining_header_bytes + 1) as u64,
);
match bounded.read_line(&mut line) {
Ok(0) | Err(_) => return None,
Ok(_) => {}
}
remaining_header_bytes =
remaining_header_bytes.checked_sub(line.len())?;
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some((name, value)) = trimmed.split_once(':')
&& name.trim().eq_ignore_ascii_case("content-length")
{
length = value.trim().parse().ok();
}
}
length
}
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()
}