use std::{
fs::OpenOptions,
io::{self, BufRead, BufReader, Read, Write},
path::PathBuf,
};
fn main() -> io::Result<()> {
let executable = std::env::current_exe()?;
let log_path = executable.with_extension("log");
let push_diagnostics = executable
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.contains("push-diagnostics"));
let stdin = io::stdin();
let mut input = BufReader::new(stdin.lock());
let stdout = io::stdout();
let mut output = stdout.lock();
let mut document_uri = None;
while let Some(body) = read_message(&mut input)? {
append_log(&log_path, &body)?;
let method = string_field(&body, "method").unwrap_or_default();
if method == "workspace/symbol" && body.contains("\"query\":\"terminate-process\"") {
eprintln!("fixture language server terminated unexpectedly");
std::process::exit(12);
}
if method == "textDocument/didOpen" {
document_uri = string_field(&body, "uri");
if push_diagnostics {
if let Some(uri) = document_uri.as_deref() {
std::thread::sleep(std::time::Duration::from_millis(50));
write_message(&mut output, &publish_diagnostics_notification(uri))?;
}
}
}
let Some(id) = request_id(&body) else {
if method == "exit" {
break;
}
continue;
};
let result = response_for(&method, document_uri.as_deref(), push_diagnostics);
write_message(
&mut output,
&format!("{{\"jsonrpc\":\"2.0\",\"id\":{id},\"result\":{result}}}"),
)?;
}
Ok(())
}
fn read_message(reader: &mut impl BufRead) -> io::Result<Option<String>> {
let mut content_length = None;
loop {
let mut line = String::new();
if reader.read_line(&mut line)? == 0 {
return Ok(None);
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some(value) = trimmed
.strip_prefix("Content-Length:")
.or_else(|| trimmed.strip_prefix("content-length:"))
{
content_length = Some(
value
.trim()
.parse::<usize>()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?,
);
}
}
let length = content_length
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length"))?;
let mut body = vec![0_u8; length];
reader.read_exact(&mut body)?;
String::from_utf8(body)
.map(Some)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
fn write_message(writer: &mut impl Write, body: &str) -> io::Result<()> {
write!(writer, "Content-Length: {}\r\n\r\n{body}", body.len())?;
writer.flush()
}
fn append_log(path: &PathBuf, body: &str) -> io::Result<()> {
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
writeln!(file, "{body}")
}
fn request_id(body: &str) -> Option<&str> {
let rest = body.split_once("\"id\":")?.1;
let end = rest.find([',', '}']).unwrap_or(rest.len());
Some(rest[..end].trim())
}
fn string_field(body: &str, field: &str) -> Option<String> {
let marker = format!("\"{field}\":\"");
let rest = body.split_once(&marker)?.1;
let mut escaped = false;
let end = rest.char_indices().find_map(|(index, character)| {
if character == '"' && !escaped {
return Some(index);
}
escaped = character == '\\' && !escaped;
if character != '\\' {
escaped = false;
}
None
})?;
Some(rest[..end].to_owned())
}
fn response_for(method: &str, uri: Option<&str>, push_diagnostics: bool) -> String {
let uri = uri.unwrap_or("file:///missing.rs");
match method {
"initialize" => initialize_response(push_diagnostics),
"textDocument/documentSymbol" => concat!(
"[{\"name\":\"answer\",\"kind\":12,",
"\"range\":{\"start\":{\"line\":0,\"character\":0},",
"\"end\":{\"line\":0,\"character\":30}},",
"\"selectionRange\":{\"start\":{\"line\":0,\"character\":7},",
"\"end\":{\"line\":0,\"character\":13}}}]"
)
.to_owned(),
"workspace/symbol" => workspace_symbol_response(uri),
"textDocument/definition"
| "textDocument/declaration"
| "textDocument/references"
| "textDocument/implementation" => locations_response(uri),
"textDocument/diagnostic" => concat!(
"{\"kind\":\"full\",\"resultId\":\"fixture-1\",\"items\":[{",
"\"range\":{\"start\":{\"line\":0,\"character\":7},",
"\"end\":{\"line\":0,\"character\":13}},",
"\"severity\":2,\"source\":\"fixture\",\"message\":\"fixture warning\"}]}"
)
.to_owned(),
"shutdown" => "null".to_owned(),
_ => "null".to_owned(),
}
}
fn initialize_response(push_diagnostics: bool) -> String {
let mut response = concat!(
"{\"capabilities\":{",
"\"positionEncoding\":\"utf-16\",",
"\"textDocumentSync\":{\"openClose\":true,\"change\":1,\"save\":true},",
"\"documentSymbolProvider\":true,\"workspaceSymbolProvider\":true,",
"\"definitionProvider\":true,\"declarationProvider\":true,",
"\"referencesProvider\":true,\"implementationProvider\":true"
)
.to_owned();
if !push_diagnostics {
response.push_str(concat!(
",\"diagnosticProvider\":{\"interFileDependencies\":false,",
"\"workspaceDiagnostics\":false}"
));
}
response.push_str("}}");
response
}
fn publish_diagnostics_notification(uri: &str) -> String {
let mut notification = concat!(
"{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",",
"\"params\":{\"uri\":\""
)
.to_owned();
notification.push_str(uri);
notification.push_str(concat!(
"\",\"version\":1,\"diagnostics\":[{",
"\"range\":{\"start\":{\"line\":0,\"character\":7},",
"\"end\":{\"line\":0,\"character\":13}},",
"\"severity\":2,\"source\":\"fixture\",",
"\"message\":\"fixture push warning\"}]}}"
));
notification
}
fn workspace_symbol_response(uri: &str) -> String {
let mut response = "[{\"name\":\"answer\",\"kind\":12,\"location\":{\"uri\":\"".to_owned();
response.push_str(uri);
response.push_str(concat!(
"\",\"range\":{\"start\":{\"line\":0,\"character\":7},",
"\"end\":{\"line\":0,\"character\":13}}}}]"
));
response
}
fn locations_response(uri: &str) -> String {
let mut response = "[{\"uri\":\"".to_owned();
response.push_str(uri);
response.push_str(concat!(
"\",\"range\":{\"start\":{\"line\":0,\"character\":7},",
"\"end\":{\"line\":0,\"character\":13}}}]"
));
response
}