Skip to content

Instantly share code, notes, and snippets.

@Mark-Simulacrum
Created April 2, 2021 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Mark-Simulacrum/c173aa652d1e327358c9279120a9896c to your computer and use it in GitHub Desktop.
Save Mark-Simulacrum/c173aa652d1e327358c9279120a9896c to your computer and use it in GitHub Desktop.
use crate::{HumanSpan, Location};
use lsp_types::Url;
use serde_derive::{Deserialize, Serialize};
use std::io::{BufRead, Write};
use std::process::Stdio;
use std::sync::atomic::AtomicUsize;
const PKG_ROOT: &str = "file:///home/mark/Build/usedef-c";
const FILE_PATH: &str = "/home/mark/Build/usedef-c/input-rs.c";
#[derive(Serialize)]
struct RpcRequest<T> {
jsonrpc: &'static str,
method: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<T>,
}
#[derive(Debug, Deserialize)]
struct RpcResponse<T> {
#[serde(default)]
id: Option<usize>,
result: Option<T>,
#[serde(default)]
error: Option<RpcResponseErr>,
}
#[derive(Debug, Deserialize)]
struct RpcResponseErr {
code: i32,
message: String,
}
#[derive(Debug)]
enum Header {
ContentLength(usize),
ContentType(String),
}
impl Header {
fn parse(s: &str) -> Header {
if s.starts_with("Content-Length:") {
let from = "Content-Length: ".len();
let to = s.find("\r").unwrap();
Header::ContentLength(s[from..to].parse::<usize>().unwrap())
} else if s.starts_with("Content-Type:") {
let from = "Content-Type: ".len();
let to = s.find("\r").unwrap();
Header::ContentType(s[from..to].to_string())
} else {
panic!("unexpected header: {:?}", s);
}
}
fn content_length(&self) -> Option<usize> {
if let Header::ContentLength(l) = *self {
Some(l)
} else {
None
}
}
}
pub(crate) struct Session {
child: std::process::Child,
stdout: std::io::BufReader<std::process::ChildStdout>,
stdin: std::process::ChildStdin,
}
impl Session {
#[allow(deprecated)]
pub(crate) fn start() -> Self {
let mut session = std::process::Command::new("clangd")
.arg("--offset-encoding=utf-8")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("launched");
let stdin = session.stdin.take().expect("stdin");
let mut stdout = std::io::BufReader::new(session.stdout.take().expect("stdout"));
assert!(send_req::<lsp_types::request::Initialize, _, _>(
&stdin,
&mut stdout,
lsp_types::InitializeParams {
process_id: None,
root_path: None,
root_uri: Some(Url::parse(PKG_ROOT).unwrap(),),
initialization_options: None,
capabilities: lsp_types::ClientCapabilities {
workspace: None,
text_document: None,
window: None,
general: None,
experimental: Some(serde_json::json!({
"offsetEncoding": ["utf-8"]
})),
},
trace: None,
workspace_folders: None,
client_info: None,
locale: None,
},
)
.is_some());
//send_not::<lsp_types::notification::Initialized, _, _>(
// &stdin,
// &mut stdout,
// lsp_types::InitializedParams {},
//);
Self {
child: session,
stdin,
stdout,
}
}
pub(crate) fn open(&mut self, text: String) {
std::fs::write(FILE_PATH, &text).unwrap();
send_not::<lsp_types::notification::DidOpenTextDocument, _, _>(
&self.stdin,
&mut self.stdout,
lsp_types::DidOpenTextDocumentParams {
text_document: lsp_types::TextDocumentItem {
uri: Url::parse(&format!("file://{}", FILE_PATH)).unwrap(),
language_id: "c".to_owned(),
version: 1,
text,
},
},
);
}
pub(crate) fn get_definition(&mut self, ident: HumanSpan) -> Option<HumanSpan> {
let resp = send_req::<lsp_types::request::GotoDefinition, _, _>(
&self.stdin,
&mut self.stdout,
lsp_types::GotoDefinitionParams {
text_document_position_params: lsp_types::TextDocumentPositionParams {
text_document: lsp_types::TextDocumentIdentifier {
uri: Url::parse(&format!("file://{}", FILE_PATH)).unwrap(),
},
position: lsp_types::Position {
line: ident.start.line as u32 - 1,
character: ident.start.col as u32,
},
},
work_done_progress_params: lsp_types::WorkDoneProgressParams {
work_done_token: None,
},
partial_result_params: lsp_types::PartialResultParams {
partial_result_token: None,
},
},
);
if let Some(Some(lsp_types::GotoDefinitionResponse::Array(a))) = resp {
if let Some(lsp_types::Location { uri, range }) = a.first() {
if uri.as_str() == format!("file://{}", FILE_PATH) {
let start = Location {
line: range.start.line + 1,
col: range.start.character,
};
let end = Location {
line: range.end.line + 1,
col: range.end.character,
};
return Some(HumanSpan { start, end });
}
}
}
None
}
pub(crate) fn shutdown(mut self) {
send_req::<lsp_types::request::Shutdown, _, _>(&self.stdin, &mut self.stdout, ());
send_not::<lsp_types::notification::Exit, _, _>(&self.stdin, &mut self.stdout, ());
self.child.stdin = Some(self.stdin);
self.child.stdout = Some(self.stdout.into_inner());
assert!(self.child.wait().unwrap().success());
}
}
static R_ID: AtomicUsize = AtomicUsize::new(1);
fn send_req<R: lsp_types::request::Request, W: Write, B: BufRead>(
to: W,
read: B,
params: R::Params,
) -> Option<R::Result> {
send(to, read, R::METHOD, params, true)
}
fn send_not<R: lsp_types::notification::Notification, W: Write, B: BufRead>(
to: W,
read: B,
params: R::Params,
) {
let _: Option<()> = send(to, read, R::METHOD, params, false);
}
fn send<R, P, W: Write, B: BufRead>(
mut to: W,
mut read: B,
method: &'static str,
params: P,
expect_response: bool,
) -> Option<R>
where
P: serde::Serialize,
R: serde::de::DeserializeOwned,
{
let id = R_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let req = RpcRequest {
jsonrpc: "2.0",
method,
id: if expect_response { Some(id) } else { None },
params: Some(params),
};
let json = serde_json::to_string(&req).unwrap();
'req: loop {
write!(to, "Content-Length: {}\r\n", json.len()).unwrap();
write!(to, "\r\n").unwrap();
//println!("sending {}", json);
to.write_all(&json.as_bytes()).unwrap();
if expect_response {
loop {
let mut headers: Vec<Header> = Vec::new();
let mut content = String::new();
loop {
let mut line = String::new();
// If the stream is closed, then return.
if read.read_line(&mut line).unwrap() == 0 {
break;
}
if line == "\r\n" {
let length = headers.iter().find_map(|h| h.content_length()).unwrap();
let mut bcontent = vec![0; length];
read.read_exact(&mut bcontent).unwrap();
content = String::from_utf8(bcontent).unwrap();
break;
} else {
headers.push(Header::parse(&line));
}
}
if content.is_empty() {
panic!("stream ended!");
} else {
if !content.contains(r#""method":"textDocument/publishDiagnostics""#) {
//eprintln!("received message: {}", content);
}
let response: RpcResponse<R> = serde_json::from_str(&content).unwrap();
if response.id != Some(id) {
continue;
}
if let Some(err) = response.error {
if err.code == -32801 {
// retry request - content modified is OK to retry
// on
eprintln!("retrying LSP request");
continue 'req;
} else {
panic!("lsp returned {:?}", err);
}
} else {
return response.result;
}
}
}
} else {
return None;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment