From 2c6aeaed7c70289ca3b9898fd1cb1a8c5b80d374 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Oct 2021 16:26:37 +0200 Subject: [PATCH] Start on integrating rust-analyzer Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- Cargo.lock | 40 +++++++ crates/gpui/src/executor.rs | 1 + crates/lsp/Cargo.toml | 15 +++ crates/lsp/src/lib.rs | 201 ++++++++++++++++++++++++++++++++ crates/project/Cargo.toml | 7 +- crates/project/src/lib.rs | 18 ++- crates/project_panel/src/lib.rs | 3 +- crates/workspace/src/lib.rs | 3 +- 8 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 crates/lsp/Cargo.toml create mode 100644 crates/lsp/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d751e25284..c514dbfe11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2949,6 +2949,34 @@ dependencies = [ "scoped-tls", ] +[[package]] +name = "lsp" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "gpui", + "lsp-types", + "parking_lot", + "serde 1.0.125", + "serde_json 1.0.64", + "smol", + "util", +] + +[[package]] +name = "lsp-types" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7801b458592d0998af808d97f6a85a6057af3aaf2a2a5c3c677702bbeb4ed7" +dependencies = [ + "bitflags 1.2.1", + "serde 1.0.125", + "serde_json 1.0.64", + "serde_repr", + "url", +] + [[package]] name = "lzw" version = "0.10.0" @@ -3762,6 +3790,7 @@ dependencies = [ "lazy_static", "libc", "log", + "lsp", "parking_lot", "postage", "rand 0.8.3", @@ -4571,6 +4600,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 01338c8a0a..32ee8fc87f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -50,6 +50,7 @@ type AnyFuture = Pin>; type AnyLocalTask = async_task::Task>; +#[must_use] pub enum Task { Local { any_task: AnyLocalTask, diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml new file mode 100644 index 0000000000..9d4c4850e9 --- /dev/null +++ b/crates/lsp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lsp" +version = "0.1.0" +edition = "2018" + +[dependencies] +gpui = { path = "../gpui" } +util = { path = "../util" } +anyhow = "1.0" +futures = "0.3" +lsp-types = "0.91" +parking_lot = "0.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +smol = "1.2" diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs new file mode 100644 index 0000000000..e05b435ba4 --- /dev/null +++ b/crates/lsp/src/lib.rs @@ -0,0 +1,201 @@ +use anyhow::{anyhow, Context, Result}; +use gpui::{executor, Task}; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use smol::{ + channel, + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + process::Command, +}; +use std::{ + collections::HashMap, + future::Future, + io::Write, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; +use std::{path::Path, process::Stdio}; +use util::TryFutureExt; + +const JSON_RPC_VERSION: &'static str = "2.0"; +const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; + +pub struct LanguageServer { + next_id: AtomicUsize, + outbound_tx: channel::Sender>, + response_handlers: Arc>>, + _input_task: Task>, + _output_task: Task>, +} + +type ResponseHandler = Box)>; + +#[derive(Serialize)] +struct Request { + jsonrpc: &'static str, + id: usize, + method: &'static str, + params: T, +} + +#[derive(Deserialize)] +struct Error { + message: String, +} + +#[derive(Deserialize)] +struct Notification<'a> { + method: String, + #[serde(borrow)] + params: &'a RawValue, +} + +#[derive(Deserialize)] +struct Response<'a> { + id: usize, + #[serde(default)] + error: Option, + #[serde(default, borrow)] + result: Option<&'a RawValue>, +} + +impl LanguageServer { + pub fn new(path: &Path, background: &executor::Background) -> Result> { + let mut server = Command::new(path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + let mut stdin = server.stdin.take().unwrap(); + let mut stdout = BufReader::new(server.stdout.take().unwrap()); + let (outbound_tx, outbound_rx) = channel::unbounded::>(); + let response_handlers = Arc::new(Mutex::new(HashMap::::new())); + let _input_task = background.spawn( + { + let response_handlers = response_handlers.clone(); + async move { + let mut buffer = Vec::new(); + loop { + buffer.clear(); + + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let message_len: usize = std::str::from_utf8(&buffer)? + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid header"))? + .trim_end() + .parse()?; + + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + if let Ok(Notification { .. }) = serde_json::from_slice(&buffer) { + } else if let Ok(Response { id, error, result }) = + serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers.lock().remove(&id) { + if let Some(result) = result { + handler(Ok(result.get())); + } else if let Some(error) = error { + handler(Err(error)); + } + } + } else { + return Err(anyhow!( + "failed to deserialize message:\n{}", + std::str::from_utf8(&buffer)? + )); + } + } + } + } + .log_err(), + ); + let _output_task = background.spawn( + async move { + let mut content_len_buffer = Vec::new(); + loop { + let message = outbound_rx.recv().await?; + write!(content_len_buffer, "{}", message.len()).unwrap(); + stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; + stdin.write_all(&content_len_buffer).await?; + stdin.write_all("\r\n\r\n".as_bytes()).await?; + stdin.write_all(&message).await?; + } + } + .log_err(), + ); + + let this = Arc::new(Self { + response_handlers, + next_id: Default::default(), + outbound_tx, + _input_task, + _output_task, + }); + let init = this.clone().init(); + background + .spawn(async move { + init.log_err().await; + }) + .detach(); + + Ok(this) + } + + async fn init(self: Arc) -> Result<()> { + let init_response = self + .request::(lsp_types::InitializeParams { + process_id: Default::default(), + root_path: Default::default(), + root_uri: Default::default(), + initialization_options: Default::default(), + capabilities: Default::default(), + trace: Default::default(), + workspace_folders: Default::default(), + client_info: Default::default(), + locale: Default::default(), + }) + .await?; + Ok(()) + } + + pub fn request( + self: &Arc, + params: T::Params, + ) -> impl Future> + where + T::Result: 'static + Send, + { + let id = self.next_id.fetch_add(1, SeqCst); + let message = serde_json::to_vec(&Request { + jsonrpc: JSON_RPC_VERSION, + id, + method: T::METHOD, + params, + }) + .unwrap(); + let mut response_handlers = self.response_handlers.lock(); + let (tx, rx) = smol::channel::bounded(1); + response_handlers.insert( + id, + Box::new(move |result| { + let response = match result { + Ok(response) => { + serde_json::from_str(response).context("failed to deserialize response") + } + Err(error) => Err(anyhow!("{}", error.message)), + }; + let _ = smol::block_on(tx.send(response)); + }), + ); + + let outbound_tx = self.outbound_tx.clone(); + async move { + outbound_tx.send(message).await?; + rx.recv().await? + } + } +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f7d87a4625..73959da5fc 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -8,15 +8,15 @@ test-support = [] [dependencies] buffer = { path = "../buffer" } +client = { path = "../client" } clock = { path = "../clock" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } -client = { path = "../client" } +lsp = { path = "../lsp" } +rpc = { path = "../rpc" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } -rpc = { path = "../rpc" } - anyhow = "1.0.38" async-trait = "0.1" futures = "0.3" @@ -35,6 +35,5 @@ toml = "0.5" client = { path = "../client", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } - rand = "0.8.3" tempdir = { version = "0.3.7" } diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 184dfd4d9c..d4e41b4f28 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -7,7 +7,8 @@ use buffer::LanguageRegistry; use client::Client; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{executor, AppContext, Entity, ModelContext, ModelHandle, Task}; +use lsp::LanguageServer; use std::{ path::Path, sync::{atomic::AtomicBool, Arc}, @@ -23,6 +24,7 @@ pub struct Project { languages: Arc, client: Arc, fs: Arc, + language_server: Arc, } pub enum Event { @@ -43,13 +45,23 @@ pub struct ProjectEntry { } impl Project { - pub fn new(languages: Arc, rpc: Arc, fs: Arc) -> Self { + pub fn new( + languages: Arc, + rpc: Arc, + fs: Arc, + background: &executor::Background, + ) -> Self { Self { worktrees: Default::default(), active_entry: None, languages, client: rpc, fs, + language_server: LanguageServer::new( + Path::new("/Users/as-cii/Downloads/rust-analyzer-x86_64-apple-darwin"), + background, + ) + .unwrap(), } } @@ -408,6 +420,6 @@ mod tests { let languages = Arc::new(LanguageRegistry::new()); let fs = Arc::new(RealFs); let rpc = client::Client::new(); - cx.add_model(|_| Project::new(languages, rpc, fs)) + cx.add_model(|cx| Project::new(languages, rpc, fs, cx.background())) } } diff --git a/crates/project_panel/src/lib.rs b/crates/project_panel/src/lib.rs index 385b7dbca2..422484e74d 100644 --- a/crates/project_panel/src/lib.rs +++ b/crates/project_panel/src/lib.rs @@ -617,11 +617,12 @@ mod tests { ) .await; - let project = cx.add_model(|_| { + let project = cx.add_model(|cx| { Project::new( params.languages.clone(), params.client.clone(), params.fs.clone(), + cx.background(), ) }); let root1 = project diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index c227ee61bd..9fafd433bb 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -322,11 +322,12 @@ pub struct Workspace { impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { - let project = cx.add_model(|_| { + let project = cx.add_model(|cx| { Project::new( params.languages.clone(), params.client.clone(), params.fs.clone(), + cx.background(), ) }); cx.observe(&project, |_, _, cx| cx.notify()).detach();