project model

This commit is contained in:
Aleksey Kladov 2018-09-02 14:46:15 +03:00
parent 7fad13de73
commit 80be61ed78
11 changed files with 251 additions and 78 deletions

View file

@ -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<CompletionItem>) {
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::<ast::IfExpr>(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") }]"#);
}

View file

@ -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" }

View file

@ -30,6 +30,7 @@ mod vfs;
mod path_map;
mod server_world;
mod project_model;
mod thread_watcher;
pub type Result<T> = ::std::result::Result<T, ::failure::Error>;
pub use caps::server_capabilities;

View file

@ -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::<Task>(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>,
task_sender: Sender<Task>,
fs_receiver: Receiver<Vec<FileEvent>>,
_ws_roots_sender: Sender<PathBuf>,
ws_receiver: Receiver<Result<CargoWorkspace>>,
state: &mut ServerWorldState,
pending_requests: &mut HashMap<u64, JobHandle>,
subs: &mut Subscriptions,
@ -87,6 +98,7 @@ fn main_loop_inner(
Msg(RawMessage),
Task(Task),
Fs(Vec<FileEvent>),
Ws(Result<CargoWorkspace>),
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::<req::DidReloadWorkspace>(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) => {

View file

@ -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;
#[derive(Debug)]
use {
Result,
thread_watcher::ThreadWatcher,
};
#[derive(Debug, Serialize, Clone)]
pub struct CargoWorkspace {
ws_members: Vec<Package>,
packages: Vec<PackageData>,
targets: Vec<TargetData>,
}
#[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<Target>
}
#[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<CargoWorkspace> {
pub fn from_cargo_metadata(path: &Path) -> Result<CargoWorkspace> {
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<PathBuf> {
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<PathBuf>, Receiver<Result<CargoWorkspace>>, ThreadWatcher) {
let (path_sender, path_receiver) = bounded::<PathBuf>(16);
let (ws_sender, ws_receiver) = bounded::<Result<CargoWorkspace>>(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)
}

View file

@ -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<CargoWorkspace>;
}

View file

@ -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<Vec<CargoWorkspace>>,
pub analysis_host: AnalysisHost,
pub path_map: PathMap,
pub mem_map: HashMap<FileId, Option<String>>,
@ -22,6 +25,7 @@ pub struct ServerWorldState {
#[derive(Clone)]
pub struct ServerWorld {
pub workspaces: Arc<Vec<CargoWorkspace>>,
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<CargoWorkspace>) {
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()
}

View file

@ -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
}
}

View file

@ -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<PathBuf>) -> (Receiver<Vec<FileEvent>>, Watcher) {
pub fn watch(roots: Vec<PathBuf>) -> (Receiver<Vec<FileEvent>>, 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<PathBuf>, sender: Sender<Vec<FileEvent>>) {

View file

@ -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::<DidReloadWorkspace>(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 ]
}
]"#
);
}

View file

@ -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<N>(
&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<R>(&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<RawMessage> {
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));