diff --git a/crates/gen_lsp_server/src/lib.rs b/crates/gen_lsp_server/src/lib.rs index 476c12cc16..0dc24ffc1f 100644 --- a/crates/gen_lsp_server/src/lib.rs +++ b/crates/gen_lsp_server/src/lib.rs @@ -15,7 +15,7 @@ mod stdio; use crossbeam_channel::{Sender, Receiver}; use languageserver_types::{ ServerCapabilities, InitializeResult, - request::{Initialize}, + request::{Initialize, Shutdown}, notification::{Initialized, Exit}, }; @@ -48,6 +48,17 @@ pub fn run_server( Ok(()) } +pub fn handle_shutdown(req: RawRequest, sender: &Sender) -> Option { + match req.cast::() { + Ok((id, ())) => { + let resp = RawResponse::ok::(id, ()); + sender.send(RawMessage::Response(resp)); + None + } + Err(req) => Some(req), + } +} + fn initialize( receiver: &mut Receiver, sender: &mut Sender, @@ -61,7 +72,7 @@ fn initialize( msg => bail!("expected initialize request, got {:?}", msg), }; - let resp = RawResponse::ok(id, InitializeResult { capabilities: caps }); + let resp = RawResponse::ok::(id, InitializeResult { capabilities: caps }); sender.send(RawMessage::Response(resp)); match receiver.recv() { Some(RawMessage::Notification(n)) => { diff --git a/crates/gen_lsp_server/src/msg.rs b/crates/gen_lsp_server/src/msg.rs index 533d7da3ec..d2ce20a114 100644 --- a/crates/gen_lsp_server/src/msg.rs +++ b/crates/gen_lsp_server/src/msg.rs @@ -87,6 +87,17 @@ impl RawMessage { } impl RawRequest { + pub fn new(id: u64, params: R::Params) -> RawRequest + where + R: Request, + R::Params: Serialize, + { + RawRequest { + id: id, + method: R::METHOD.to_string(), + params: to_value(¶ms).unwrap(), + } + } pub fn cast(self) -> ::std::result::Result<(u64, R::Params), RawRequest> where R: Request, @@ -102,7 +113,10 @@ impl RawRequest { } impl RawResponse { - pub fn ok(id: u64, result: impl Serialize) -> RawResponse { + pub fn ok(id: u64, result: R::Result) -> RawResponse + where R: Request, + R::Result: Serialize, + { RawResponse { id, result: Some(to_value(&result).unwrap()), diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 32c1219e1d..2a9374e984 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -24,3 +24,6 @@ libsyntax2 = { path = "../libsyntax2" } libeditor = { path = "../libeditor" } libanalysis = { path = "../libanalysis" } gen_lsp_server = { path = "../gen_lsp_server" } + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 5bbd210447..bfa4bc41e8 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -21,7 +21,7 @@ extern crate im; extern crate relative_path; mod caps; -mod req; +pub mod req; mod conv; mod main_loop; mod vfs; diff --git a/crates/server/src/main_loop/mod.rs b/crates/server/src/main_loop/mod.rs index 610aa42643..ff267fcade 100644 --- a/crates/server/src/main_loop/mod.rs +++ b/crates/server/src/main_loop/mod.rs @@ -11,7 +11,10 @@ use serde::{Serialize, de::DeserializeOwned}; use crossbeam_channel::{bounded, Sender, Receiver}; use languageserver_types::{NumberOrString}; use libanalysis::{FileId, JobHandle, JobToken}; -use gen_lsp_server::{RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode}; +use gen_lsp_server::{ + RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode, + handle_shutdown, +}; use { req, @@ -21,6 +24,7 @@ use { main_loop::subscriptions::{Subscriptions}, }; +#[derive(Debug)] enum Task { Respond(RawResponse), Notify(RawNotification), @@ -40,7 +44,7 @@ pub fn main_loop( let mut pending_requests = HashMap::new(); let mut subs = Subscriptions::new(); - main_loop_inner( + let res = main_loop_inner( &pool, msg_receriver, msg_sender, @@ -50,17 +54,19 @@ pub fn main_loop( &mut state, &mut pending_requests, &mut subs, - )?; + ); - info!("waiting for background jobs to finish..."); + info!("waiting for tasks to finish..."); task_receiver.for_each(|task| on_task(task, msg_sender, &mut pending_requests)); + info!("...tasks have finished"); + info!("joining threadpool..."); pool.join(); - info!("...background jobs have finished"); + info!("...threadpool has finished"); info!("waiting for file watcher to finish..."); watcher.stop()?; info!("...file watcher has finished"); - Ok(()) + res } fn main_loop_inner( @@ -73,15 +79,17 @@ fn main_loop_inner( state: &mut ServerWorldState, pending_requests: &mut HashMap, subs: &mut Subscriptions, -) -> Result { +) -> Result<()> { let mut fs_receiver = Some(fs_receiver); loop { + #[derive(Debug)] enum Event { Msg(RawMessage), Task(Task), Fs(Vec), FsWatcherDead, } + trace!("selecting"); let event = select! { recv(msg_receiver, msg) => match msg { Some(msg) => Event::Msg(msg), @@ -93,6 +101,7 @@ fn main_loop_inner( None => Event::FsWatcherDead, } }; + trace!("selected {:?}", event); let mut state_changed = false; match event { Event::FsWatcherDead => fs_receiver = None, @@ -105,9 +114,9 @@ fn main_loop_inner( Event::Msg(msg) => { match msg { RawMessage::Request(req) => { - let req = match req.cast::() { - Ok((id, _params)) => return Ok(id), - Err(req) => req, + let req = match handle_shutdown(req, msg_sender) { + Some(req) => req, + None => return Ok(()), }; match on_request(state, pending_requests, pool, &task_sender, req)? { None => (), @@ -290,7 +299,7 @@ impl<'a> PoolDispatcher<'a> { let sender = self.sender.clone(); self.pool.execute(move || { let resp = match f(world, params, token) { - Ok(resp) => RawResponse::ok(id, resp), + Ok(resp) => RawResponse::ok::(id, resp), Err(e) => RawResponse::err(id, ErrorCode::InternalError as i32, e.to_string()), }; let task = Task::Respond(resp); diff --git a/crates/server/src/req.rs b/crates/server/src/req.rs index f521272719..893cbde816 100644 --- a/crates/server/src/req.rs +++ b/crates/server/src/req.rs @@ -127,7 +127,7 @@ impl Request for Runnables { const METHOD: &'static str = "m/runnables"; } -#[derive(Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct RunnablesParams { pub text_document: TextDocumentIdentifier, diff --git a/crates/server/src/vfs.rs b/crates/server/src/vfs.rs index a5c3674940..2e4319cdb7 100644 --- a/crates/server/src/vfs.rs +++ b/crates/server/src/vfs.rs @@ -11,11 +11,13 @@ use walkdir::WalkDir; use Result; +#[derive(Debug)] pub struct FileEvent { pub path: PathBuf, pub kind: FileEventKind, } +#[derive(Debug)] pub enum FileEventKind { Add(String), #[allow(unused)] diff --git a/crates/server/tests/heavy_tests/main.rs b/crates/server/tests/heavy_tests/main.rs new file mode 100644 index 0000000000..94c8243b06 --- /dev/null +++ b/crates/server/tests/heavy_tests/main.rs @@ -0,0 +1,42 @@ +extern crate tempdir; +extern crate crossbeam_channel; +extern crate languageserver_types; +extern crate serde; +extern crate serde_json; +extern crate gen_lsp_server; +extern crate flexi_logger; +extern crate m; + +mod support; + +use m::req::{Runnables, RunnablesParams}; + +use support::project; + +#[test] +fn test_runnables() { + let server = project(r" +//- lib.rs +#[test] +fn foo() { +} +"); + server.request::( + RunnablesParams { + text_document: server.doc_id("lib.rs"), + position: None, + }, + r#"[ + { + "args": [ "test", "--", "foo", "--nocapture" ], + "bin": "cargo", + "env": { "RUST_BACKTRACE": "short" }, + "label": "test foo", + "range": { + "end": { "character": 1, "line": 2 }, + "start": { "character": 0, "line": 0 } + } + } + ]"# + ); +} diff --git a/crates/server/tests/heavy_tests/support.rs b/crates/server/tests/heavy_tests/support.rs new file mode 100644 index 0000000000..113ef4c542 --- /dev/null +++ b/crates/server/tests/heavy_tests/support.rs @@ -0,0 +1,169 @@ +use std::{ + fs, + thread, + cell::Cell, + path::PathBuf, +}; + +use tempdir::TempDir; +use crossbeam_channel::{bounded, Sender, Receiver}; +use flexi_logger::Logger; +use languageserver_types::{ + Url, + TextDocumentIdentifier, + request::{Request, Shutdown}, + notification::DidOpenTextDocument, + DidOpenTextDocumentParams, + TextDocumentItem, +}; +use serde::Serialize; +use serde_json::{Value, from_str, to_string_pretty}; +use gen_lsp_server::{RawMessage, RawRequest, RawNotification}; + +use m::{Result, main_loop}; + +pub fn project(fixture: &str) -> Server { + Logger::with_env_or_str("").start().unwrap(); + + let tmp_dir = TempDir::new("test-project") + .unwrap(); + let mut buf = String::new(); + let mut file_name = None; + let mut paths = vec![]; + macro_rules! flush { + () => { + if let Some(file_name) = file_name { + let path = tmp_dir.path().join(file_name); + fs::write(path.as_path(), buf.as_bytes()).unwrap(); + paths.push((path, buf.clone())); + } + } + }; + for line in fixture.lines() { + if line.starts_with("//-") { + flush!(); + buf.clear(); + file_name = Some(line["//-".len()..].trim()); + continue; + } + buf.push_str(line); + buf.push('\n'); + } + flush!(); + + Server::new(tmp_dir, paths) +} + +pub struct Server { + req_id: Cell, + dir: TempDir, + sender: Option>, + receiver: Receiver, + server: Option>>, +} + +impl Server { + fn new(dir: TempDir, files: Vec<(PathBuf, String)>) -> Server { + let path = dir.path().to_path_buf(); + let (client_sender, mut server_receiver) = bounded(1); + let (mut server_sender, client_receiver) = bounded(1); + let server = thread::spawn(move || main_loop(path, &mut server_receiver, &mut server_sender)); + let res = Server { + req_id: Cell::new(1), + dir, + sender: Some(client_sender), + receiver: client_receiver, + server: Some(server), + }; + for (path, text) in files { + res.send_notification(RawNotification::new::( + DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: Url::from_file_path(path).unwrap(), + language_id: "rust".to_string(), + version: 0, + text, + } + } + )) + } + res + } + + pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { + let path = self.dir.path().join(rel_path); + TextDocumentIdentifier { + uri: Url::from_file_path(path).unwrap(), + } + } + + pub fn request( + &self, + params: R::Params, + expected_resp: &str, + ) + where + R: Request, + R::Params: Serialize, + { + let id = self.req_id.get(); + self.req_id.set(id + 1); + let expected_resp: Value = from_str(expected_resp).unwrap(); + let actual = self.send_request::(id, params); + assert_eq!( + expected_resp, actual, + "Expected:\n{}\n\ + Actual:\n{}\n", + to_string_pretty(&expected_resp).unwrap(), + to_string_pretty(&actual).unwrap(), + ); + } + + fn send_request(&self, id: u64, params: R::Params) -> Value + where + R: Request, + R::Params: Serialize, + { + let r = RawRequest::new::(id, params); + self.sender.as_ref() + .unwrap() + .send(RawMessage::Request(r)); + + while let Some(msg) = self.receiver.recv() { + match msg { + RawMessage::Request(req) => panic!("unexpected request: {:?}", req), + RawMessage::Notification(_) => (), + RawMessage::Response(res) => { + assert_eq!(res.id, id); + if let Some(err) = res.error { + panic!("error response: {:#?}", err); + } + return res.result.unwrap(); + } + } + } + panic!("no response"); + } + fn send_notification(&self, not: RawNotification) { + + self.sender.as_ref() + .unwrap() + .send(RawMessage::Notification(not)); + } +} + +impl Drop for Server { + fn drop(&mut self) { + { + self.send_request::(666, ()); + drop(self.sender.take().unwrap()); + while let Some(msg) = self.receiver.recv() { + drop(msg); + } + } + eprintln!("joining server"); + self.server.take() + .unwrap() + .join().unwrap().unwrap(); + } +}