From 80be61ed78e8410e013cb94879435d54a4907c30 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 2 Sep 2018 14:46:15 +0300 Subject: project model --- crates/libeditor/src/completion.rs | 52 ++++++++++++------------ crates/server/Cargo.toml | 3 +- crates/server/src/lib.rs | 1 + crates/server/src/main_loop/mod.rs | 39 +++++++++++++++--- crates/server/src/project_model.rs | 54 ++++++++++++++++++++----- crates/server/src/req.rs | 8 ++++ crates/server/src/server_world.rs | 10 ++++- crates/server/src/thread_watcher.rs | 33 +++++++++++++++ crates/server/src/vfs.rs | 28 +++---------- crates/server/tests/heavy_tests/main.rs | 36 ++++++++++++++++- crates/server/tests/heavy_tests/support.rs | 65 +++++++++++++++++++++++++----- 11 files changed, 251 insertions(+), 78 deletions(-) create mode 100644 crates/server/src/thread_watcher.rs (limited to 'crates') diff --git a/crates/libeditor/src/completion.rs b/crates/libeditor/src/completion.rs index be37fb6bf..b296f6fd5 100644 --- a/crates/libeditor/src/completion.rs +++ b/crates/libeditor/src/completion.rs @@ -62,16 +62,16 @@ fn is_single_segment(name_ref: ast::NameRef) -> bool { } fn complete_expr_keywords(file: &File, fn_def: ast::FnDef, name_ref: ast::NameRef, acc: &mut Vec) { - acc.push(keyword("if", "if $0 { }")); - acc.push(keyword("match", "match $0 { }")); - acc.push(keyword("while", "while $0 { }")); + acc.push(keyword("if", "if $0 {}")); + acc.push(keyword("match", "match $0 {}")); + acc.push(keyword("while", "while $0 {}")); acc.push(keyword("loop", "loop {$0}")); if let Some(off) = name_ref.syntax().range().start().checked_sub(2.into()) { if let Some(if_expr) = find_node_at_offset::(file.syntax(), off) { if if_expr.syntax().range().end() < name_ref.syntax().range().start() { acc.push(keyword("else", "else {$0}")); - acc.push(keyword("else if", "else if $0 { }")); + acc.push(keyword("else if", "else if $0 {}")); } } } @@ -276,9 +276,9 @@ mod tests { fn quux() { <|> } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "return", snippet: Some("return") }]"#); } @@ -291,12 +291,12 @@ mod tests { () } <|> } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "else", snippet: Some("else {$0}") }, - CompletionItem { name: "else if", snippet: Some("else if $0 { }") }, + CompletionItem { name: "else if", snippet: Some("else if $0 {}") }, CompletionItem { name: "return", snippet: Some("return") }]"#); } @@ -307,9 +307,9 @@ mod tests { <|> 92 } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "return", snippet: Some("return $0;") }]"#); check_snippet_completion(r" @@ -317,9 +317,9 @@ mod tests { <|> 92 } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "return", snippet: Some("return;") }]"#); } @@ -332,9 +332,9 @@ mod tests { () => <|> } } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "return", snippet: Some("return $0") }]"#); } @@ -345,9 +345,9 @@ mod tests { fn quux() -> i32 { loop { <|> } } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "continue", snippet: Some("continue") }, CompletionItem { name: "break", snippet: Some("break") }, @@ -356,9 +356,9 @@ mod tests { fn quux() -> i32 { loop { || { <|> } } } - ", r#"[CompletionItem { name: "if", snippet: Some("if $0 { }") }, - CompletionItem { name: "match", snippet: Some("match $0 { }") }, - CompletionItem { name: "while", snippet: Some("while $0 { }") }, + ", r#"[CompletionItem { name: "if", snippet: Some("if $0 {}") }, + CompletionItem { name: "match", snippet: Some("match $0 {}") }, + CompletionItem { name: "while", snippet: Some("while $0 {}") }, CompletionItem { name: "loop", snippet: Some("loop {$0}") }, CompletionItem { name: "return", snippet: Some("return $0") }]"#); } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index c3e7a6238..cb96929c6 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -18,8 +18,9 @@ url_serde = "0.2.0" languageserver-types = "0.49.0" walkdir = "2.2.0" im = { version = "11.0.1", features = ["arc"] } -text_unit = { version = "0.1.2", features = ["serde"] } cargo_metadata = "0.6.0" +text_unit = { version = "0.1.2", features = ["serde"] } +smol_str = { version = "0.1.5", features = ["serde"] } libsyntax2 = { path = "../libsyntax2" } libeditor = { path = "../libeditor" } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 096b94a6d..d874ecf84 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -30,6 +30,7 @@ mod vfs; mod path_map; mod server_world; mod project_model; +mod thread_watcher; pub type Result = ::std::result::Result; pub use caps::server_capabilities; diff --git a/crates/server/src/main_loop/mod.rs b/crates/server/src/main_loop/mod.rs index ff267fcad..52fc00c9c 100644 --- a/crates/server/src/main_loop/mod.rs +++ b/crates/server/src/main_loop/mod.rs @@ -22,6 +22,7 @@ use { vfs::{self, FileEvent}, server_world::{ServerWorldState, ServerWorld}, main_loop::subscriptions::{Subscriptions}, + project_model::{CargoWorkspace, workspace_loader}, }; #[derive(Debug)] @@ -37,20 +38,24 @@ pub fn main_loop( ) -> Result<()> { let pool = ThreadPool::new(4); let (task_sender, task_receiver) = bounded::(16); - let (fs_events_receiver, watcher) = vfs::watch(vec![root]); + let (fs_events_receiver, watcher) = vfs::watch(vec![root.clone()]); + let (ws_root_sender, ws_receiver, ws_watcher) = workspace_loader(); + ws_root_sender.send(root); info!("server initialized, serving requests"); let mut state = ServerWorldState::new(); let mut pending_requests = HashMap::new(); let mut subs = Subscriptions::new(); - let res = main_loop_inner( + let main_res = main_loop_inner( &pool, msg_receriver, msg_sender, task_receiver.clone(), task_sender, fs_events_receiver, + ws_root_sender, + ws_receiver, &mut state, &mut pending_requests, &mut subs, @@ -63,10 +68,14 @@ pub fn main_loop( pool.join(); info!("...threadpool has finished"); - info!("waiting for file watcher to finish..."); - watcher.stop()?; - info!("...file watcher has finished"); - res + let vfs_res = watcher.stop(); + let ws_res = ws_watcher.stop(); + + main_res?; + vfs_res?; + ws_res?; + + Ok(()) } fn main_loop_inner( @@ -76,6 +85,8 @@ fn main_loop_inner( task_receiver: Receiver, task_sender: Sender, fs_receiver: Receiver>, + _ws_roots_sender: Sender, + ws_receiver: Receiver>, state: &mut ServerWorldState, pending_requests: &mut HashMap, subs: &mut Subscriptions, @@ -87,6 +98,7 @@ fn main_loop_inner( Msg(RawMessage), Task(Task), Fs(Vec), + Ws(Result), FsWatcherDead, } trace!("selecting"); @@ -100,6 +112,10 @@ fn main_loop_inner( Some(events) => Event::Fs(events), None => Event::FsWatcherDead, } + recv(ws_receiver, ws) => match ws { + None => bail!("workspace watcher died"), + Some(ws) => Event::Ws(ws), + } }; trace!("selected {:?}", event); let mut state_changed = false; @@ -111,6 +127,17 @@ fn main_loop_inner( state.apply_fs_changes(events); state_changed = true; } + Event::Ws(ws) => { + match ws { + Ok(ws) => { + let not = RawNotification::new::(vec![ws.clone()]); + msg_sender.send(RawMessage::Notification(not)); + state.set_workspaces(vec![ws]); + state_changed = true; + } + Err(e) => warn!("loading workspace failed: {}", e), + } + } Event::Msg(msg) => { match msg { RawMessage::Request(req) => { diff --git a/crates/server/src/project_model.rs b/crates/server/src/project_model.rs index a33b34dd0..1c5954dad 100644 --- a/crates/server/src/project_model.rs +++ b/crates/server/src/project_model.rs @@ -2,30 +2,35 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, }; -use libsyntax2::SmolStr; use cargo_metadata::{metadata_run, CargoOpt}; -use Result; +use crossbeam_channel::{bounded, Sender, Receiver}; +use libsyntax2::SmolStr; + +use { + Result, + thread_watcher::ThreadWatcher, +}; -#[derive(Debug)] +#[derive(Debug, Serialize, Clone)] pub struct CargoWorkspace { ws_members: Vec, packages: Vec, targets: Vec, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize)] pub struct Package(usize); -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize)] pub struct Target(usize); -#[derive(Debug)] +#[derive(Debug, Serialize, Clone)] struct PackageData { name: SmolStr, manifest: PathBuf, targets: Vec } -#[derive(Debug)] +#[derive(Debug, Serialize, Clone)] struct TargetData { pkg: Package, name: SmolStr, @@ -33,7 +38,7 @@ struct TargetData { kind: TargetKind, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)] pub enum TargetKind { Bin, Lib, Example, Test, Bench, Other, } @@ -66,9 +71,10 @@ impl Target { } impl CargoWorkspace { - pub fn from_path(path: &Path) -> Result { + pub fn from_cargo_metadata(path: &Path) -> Result { + let cargo_toml = find_cargo_toml(path)?; let meta = metadata_run( - Some(path), + Some(cargo_toml.as_path()), true, Some(CargoOpt::AllFeatures) ).map_err(|e| format_err!("cargo metadata failed: {}", e))?; @@ -121,6 +127,21 @@ impl CargoWorkspace { } } +fn find_cargo_toml(path: &Path) -> Result { + if path.ends_with("Cargo.toml") { + return Ok(path.to_path_buf()); + } + let mut curr = Some(path); + while let Some(path) = curr { + let candidate = path.join("Cargo.toml"); + if candidate.exists() { + return Ok(candidate); + } + curr = path.parent(); + } + bail!("can't find Cargo.toml at {}", path.display()) +} + impl TargetKind { fn new(kinds: &[String]) -> TargetKind { for kind in kinds { @@ -136,3 +157,16 @@ impl TargetKind { TargetKind::Other } } + +pub fn workspace_loader() -> (Sender, Receiver>, ThreadWatcher) { + let (path_sender, path_receiver) = bounded::(16); + let (ws_sender, ws_receiver) = bounded::>(1); + let thread = ThreadWatcher::spawn("workspace loader", move || { + path_receiver + .into_iter() + .map(|path| CargoWorkspace::from_cargo_metadata(path.as_path())) + .for_each(|it| ws_sender.send(it)) + }); + + (path_sender, ws_receiver, thread) +} diff --git a/crates/server/src/req.rs b/crates/server/src/req.rs index 893cbde81..b9e0c3796 100644 --- a/crates/server/src/req.rs +++ b/crates/server/src/req.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use languageserver_types::{TextDocumentIdentifier, Range, Url, Position, Location}; use url_serde; +use project_model::CargoWorkspace; pub use languageserver_types::{ request::*, notification::*, @@ -167,3 +168,10 @@ pub enum FileSystemEdit { dst: Url, } } + +pub enum DidReloadWorkspace {} + +impl Notification for DidReloadWorkspace { + const METHOD: &'static str = "m/didReloadWorkspace"; + type Params = Vec; +} diff --git a/crates/server/src/server_world.rs b/crates/server/src/server_world.rs index d99ef661e..4d5c50428 100644 --- a/crates/server/src/server_world.rs +++ b/crates/server/src/server_world.rs @@ -2,6 +2,7 @@ use std::{ fs, path::{PathBuf, Path}, collections::HashMap, + sync::Arc, }; use languageserver_types::Url; @@ -11,10 +12,12 @@ use { Result, path_map::PathMap, vfs::{FileEvent, FileEventKind}, + project_model::CargoWorkspace, }; #[derive(Debug)] pub struct ServerWorldState { + pub workspaces: Arc>, pub analysis_host: AnalysisHost, pub path_map: PathMap, pub mem_map: HashMap>, @@ -22,6 +25,7 @@ pub struct ServerWorldState { #[derive(Clone)] pub struct ServerWorld { + pub workspaces: Arc>, pub analysis: Analysis, pub path_map: PathMap, } @@ -29,6 +33,7 @@ pub struct ServerWorld { impl ServerWorldState { pub fn new() -> ServerWorldState { ServerWorldState { + workspaces: Arc::new(Vec::new()), analysis_host: AnalysisHost::new(), path_map: PathMap::new(), mem_map: HashMap::new(), @@ -89,9 +94,12 @@ impl ServerWorldState { self.analysis_host.change_file(file_id, text); Ok(file_id) } - + pub fn set_workspaces(&mut self, ws: Vec) { + self.workspaces = Arc::new(ws); + } pub fn snapshot(&self) -> ServerWorld { ServerWorld { + workspaces: Arc::clone(&self.workspaces), analysis: self.analysis_host.analysis(self.path_map.clone()), path_map: self.path_map.clone() } diff --git a/crates/server/src/thread_watcher.rs b/crates/server/src/thread_watcher.rs new file mode 100644 index 000000000..98bcdfd6c --- /dev/null +++ b/crates/server/src/thread_watcher.rs @@ -0,0 +1,33 @@ +use std::thread; +use drop_bomb::DropBomb; +use Result; + +pub struct ThreadWatcher { + name: &'static str, + thread: thread::JoinHandle<()>, + bomb: DropBomb, +} + +impl ThreadWatcher { + pub fn spawn(name: &'static str, f: impl FnOnce() + Send + 'static) -> ThreadWatcher { + let thread = thread::spawn(f); + ThreadWatcher { + name, + thread, + bomb: DropBomb::new(format!("ThreadWatcher {} was not stopped", name)), + } + } + + pub fn stop(mut self) -> Result<()> { + info!("waiting for {} to finish ...", self.name); + let name = self.name; + self.bomb.defuse(); + let res = self.thread.join() + .map_err(|_| format_err!("ThreadWatcher {} died", name)); + match &res { + Ok(()) => info!("... {} terminated with ok", name), + Err(_) => error!("... {} terminated with err", name) + } + res + } +} diff --git a/crates/server/src/vfs.rs b/crates/server/src/vfs.rs index 2e4319cdb..2acc3f55f 100644 --- a/crates/server/src/vfs.rs +++ b/crates/server/src/vfs.rs @@ -1,14 +1,14 @@ use std::{ path::PathBuf, - thread, fs, }; use crossbeam_channel::{Sender, Receiver, bounded}; -use drop_bomb::DropBomb; use walkdir::WalkDir; -use Result; +use { + thread_watcher::ThreadWatcher, +}; #[derive(Debug)] @@ -24,26 +24,10 @@ pub enum FileEventKind { Remove, } -pub struct Watcher { - thread: thread::JoinHandle<()>, - bomb: DropBomb, -} - -impl Watcher { - pub fn stop(mut self) -> Result<()> { - self.bomb.defuse(); - self.thread.join() - .map_err(|_| format_err!("file watcher died")) - } -} - -pub fn watch(roots: Vec) -> (Receiver>, Watcher) { +pub fn watch(roots: Vec) -> (Receiver>, ThreadWatcher) { let (sender, receiver) = bounded(16); - let thread = thread::spawn(move || run(roots, sender)); - (receiver, Watcher { - thread, - bomb: DropBomb::new("Watcher should be stopped explicitly"), - }) + let watcher = ThreadWatcher::spawn("vfs", move || run(roots, sender)); + (receiver, watcher) } fn run(roots: Vec, sender: Sender>) { diff --git a/crates/server/tests/heavy_tests/main.rs b/crates/server/tests/heavy_tests/main.rs index 94c8243b0..9c0196f22 100644 --- a/crates/server/tests/heavy_tests/main.rs +++ b/crates/server/tests/heavy_tests/main.rs @@ -1,5 +1,6 @@ -extern crate tempdir; +#[macro_use] extern crate crossbeam_channel; +extern crate tempdir; extern crate languageserver_types; extern crate serde; extern crate serde_json; @@ -9,10 +10,12 @@ extern crate m; mod support; -use m::req::{Runnables, RunnablesParams}; +use m::req::{Runnables, RunnablesParams, DidReloadWorkspace}; use support::project; +const LOG: &'static str = "WARN"; + #[test] fn test_runnables() { let server = project(r" @@ -40,3 +43,32 @@ fn foo() { ]"# ); } + +#[test] +fn test_project_model() { + let server = project(r#" +//- Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- src/lib.rs +pub fn foo() {} +"#); + server.notification::(r#"[ + { + "packages": [ + { + "manifest": "$PROJECT_ROOT$/Cargo.toml", + "name": "foo", + "targets": [ 0 ] + } + ], + "targets": [ + { "kind": "Lib", "name": "foo", "pkg": 0, "root": "$PROJECT_ROOT$/src/lib.rs" } + ], + "ws_members": [ 0 ] + } +]"# + ); +} diff --git a/crates/server/tests/heavy_tests/support.rs b/crates/server/tests/heavy_tests/support.rs index 36ca56af3..006926216 100644 --- a/crates/server/tests/heavy_tests/support.rs +++ b/crates/server/tests/heavy_tests/support.rs @@ -3,16 +3,18 @@ use std::{ thread, cell::{Cell, RefCell}, path::PathBuf, + time::Duration, + sync::Once, }; use tempdir::TempDir; -use crossbeam_channel::{bounded, Sender, Receiver}; +use crossbeam_channel::{bounded, after, Sender, Receiver}; use flexi_logger::Logger; use languageserver_types::{ Url, TextDocumentIdentifier, request::{Request, Shutdown}, - notification::DidOpenTextDocument, + notification::{Notification, DidOpenTextDocument}, DidOpenTextDocumentParams, TextDocumentItem, }; @@ -23,7 +25,8 @@ 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(); + static INIT: Once = Once::new(); + INIT.call_once(|| Logger::with_env_or_str(::LOG).start().unwrap()); let tmp_dir = TempDir::new("test-project") .unwrap(); @@ -34,6 +37,7 @@ pub fn project(fixture: &str) -> Server { () => { if let Some(file_name) = file_name { let path = tmp_dir.path().join(file_name); + fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path.as_path(), buf.as_bytes()).unwrap(); paths.push((path, buf.clone())); } @@ -121,6 +125,25 @@ impl Server { ); } + pub fn notification( + &self, + expected: &str, + ) + where + N: Notification, + { + let expected = expected.replace("$PROJECT_ROOT$", &self.dir.path().display().to_string()); + let expected: Value = from_str(&expected).unwrap(); + let actual = self.wait_for_notification(N::METHOD); + assert_eq!( + expected, actual, + "Expected:\n{}\n\ + Actual:\n{}\n", + to_string_pretty(&expected).unwrap(), + to_string_pretty(&actual).unwrap(), + ); + } + fn send_request(&self, id: u64, params: R::Params) -> Value where R: Request, @@ -130,7 +153,6 @@ impl Server { self.sender.as_ref() .unwrap() .send(RawMessage::Request(r)); - while let Some(msg) = self.recv() { match msg { RawMessage::Request(req) => panic!("unexpected request: {:?}", req), @@ -146,15 +168,38 @@ impl Server { } panic!("no response"); } + fn wait_for_notification(&self, method: &str) -> Value { + let f = |msg: &RawMessage| match msg { + RawMessage::Notification(n) if n.method == method => { + Some(n.params.clone()) + } + _ => None, + }; + + for msg in self.messages.borrow().iter() { + if let Some(res) = f(msg) { + return res; + } + } + while let Some(msg) = self.recv() { + if let Some(res) = f(&msg) { + return res; + } + } + panic!("no response") + } fn recv(&self) -> Option { - self.receiver.recv() - .map(|msg| { - self.messages.borrow_mut().push(msg.clone()); - msg - }) + let timeout = Duration::from_secs(5); + let msg = select! { + recv(&self.receiver, msg) => msg, + recv(after(timeout)) => panic!("timed out"), + }; + msg.map(|msg| { + self.messages.borrow_mut().push(msg.clone()); + msg + }) } fn send_notification(&self, not: RawNotification) { - self.sender.as_ref() .unwrap() .send(RawMessage::Notification(not)); -- cgit v1.2.3