-
-
Save Mark-Simulacrum/c173aa652d1e327358c9279120a9896c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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