1137: Adds support for multiple editor workspaces on initialization r=matklad a=jrvidal

OK, so this "simple hack" turned out to be way more contrived than I expected 😂

### What works
This patch only handles multi-folder editor workspaces _on initialization_.
  * I've found that modifying the layout of a workspace in VSCode just reloads the extension, so this hack should be enough for now.
  * Not sure about how emacs-lsp behaves, but we fallback gracefully to the mono-folder workspace, so it should be fine.

### What doesn't work
* [x] `cargo watch` can only watch a single root folder with a `Cargo.toml`. I've left this part untouched but we could either warn that it's not supported or launch _multiple_ `cargo-watch` processes.
* [x] The `rust-analyzer/runnables` command is not functional, since we don't send the correct `cwd`.
* [x] Should we add some happy path test to `heavy_tests`?
* [ ] Going from a single `root` to multiple `roots` leaves us with a couple of `n * m` loops that smell a bit. The number of folders in the editor workspace is probably low though.

Co-authored-by: Roberto Vidal <vidal.roberto.j@gmail.com>
This commit is contained in:
bors[bot] 2019-04-14 17:26:07 +00:00
commit 88be6f3217
11 changed files with 145 additions and 64 deletions

View file

@ -40,12 +40,23 @@ fn main_inner() -> Result<()> {
run_server(ra_lsp_server::server_capabilities(), receiver, sender, |params, r, s| {
let root = params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd);
let workspace_roots = params
.workspace_folders
.map(|workspaces| {
workspaces
.into_iter()
.filter_map(|it| it.uri.to_file_path().ok())
.collect::<Vec<_>>()
})
.filter(|workspaces| !workspaces.is_empty())
.unwrap_or_else(|| vec![root]);
let opts = params
.initialization_options
.and_then(|v| InitializationOptions::deserialize(v).ok())
.unwrap_or(InitializationOptions::default());
ra_lsp_server::main_loop(root, opts, r, s)
ra_lsp_server::main_loop(workspace_roots, opts, r, s)
})?;
log::info!("shutting down IO...");
threads.join()?;

View file

@ -48,7 +48,7 @@ enum Task {
const THREADPOOL_SIZE: usize = 8;
pub fn main_loop(
ws_root: PathBuf,
ws_roots: Vec<PathBuf>,
options: InitializationOptions,
msg_receiver: &Receiver<RawMessage>,
msg_sender: &Sender<RawMessage>,
@ -59,23 +59,26 @@ pub fn main_loop(
// FIXME: support dynamic workspace loading.
let workspaces = {
let ws_worker = workspace_loader();
ws_worker.sender().send(ws_root.clone()).unwrap();
match ws_worker.receiver().recv().unwrap() {
Ok(ws) => vec![ws],
Err(e) => {
log::error!("loading workspace failed: {}", e);
let mut loaded_workspaces = Vec::new();
for ws_root in &ws_roots {
ws_worker.sender().send(ws_root.clone()).unwrap();
match ws_worker.receiver().recv().unwrap() {
Ok(ws) => loaded_workspaces.push(ws),
Err(e) => {
log::error!("loading workspace failed: {}", e);
show_message(
req::MessageType::Error,
format!("rust-analyzer failed to load workspace: {}", e),
msg_sender,
);
Vec::new()
show_message(
req::MessageType::Error,
format!("rust-analyzer failed to load workspace: {}", e),
msg_sender,
);
}
}
}
loaded_workspaces
};
let mut state = ServerWorldState::new(ws_root.clone(), workspaces);
let mut state = ServerWorldState::new(ws_roots, workspaces);
log::info!("server initialized, serving requests");

View file

@ -263,6 +263,7 @@ pub fn handle_runnables(
let line_index = world.analysis().file_line_index(file_id);
let offset = params.position.map(|it| it.conv_with(&line_index));
let mut res = Vec::new();
let workspace_root = world.workspace_root_for(file_id);
for runnable in world.analysis().runnables(file_id)? {
if let Some(offset) = offset {
if !runnable.range.contains_inclusive(offset) {
@ -287,6 +288,7 @@ pub fn handle_runnables(
m.insert("RUST_BACKTRACE".to_string(), "short".to_string());
m
},
cwd: workspace_root.map(|root| root.to_string_lossy().to_string()),
};
res.push(r);
}
@ -309,6 +311,7 @@ pub fn handle_runnables(
bin: "cargo".to_string(),
args: check_args,
env: FxHashMap::default(),
cwd: workspace_root.map(|root| root.to_string_lossy().to_string()),
});
Ok(res)
}
@ -627,6 +630,7 @@ pub fn handle_code_lens(
let line_index = world.analysis().file_line_index(file_id);
let mut lenses: Vec<CodeLens> = Default::default();
let workspace_root = world.workspace_root_for(file_id);
// Gather runnables
for runnable in world.analysis().runnables(file_id)? {
@ -647,6 +651,7 @@ pub fn handle_code_lens(
bin: "cargo".into(),
args,
env: Default::default(),
cwd: workspace_root.map(|root| root.to_string_lossy().to_string()),
};
let lens = CodeLens {

View file

@ -163,6 +163,7 @@ pub struct Runnable {
pub bin: String,
pub args: Vec<String>,
pub env: FxHashMap<String, String>,
pub cwd: Option<String>,
}
#[derive(Serialize, Debug)]

View file

@ -1,5 +1,5 @@
use std::{
path::PathBuf,
path::{Path, PathBuf},
sync::Arc,
};
@ -24,7 +24,7 @@ use crate::{
#[derive(Debug)]
pub struct ServerWorldState {
pub roots_to_scan: usize,
pub root: PathBuf,
pub roots: Vec<PathBuf>,
pub workspaces: Arc<Vec<ProjectWorkspace>>,
pub analysis_host: AnalysisHost,
pub vfs: Arc<RwLock<Vfs>>,
@ -37,19 +37,20 @@ pub struct ServerWorld {
}
impl ServerWorldState {
pub fn new(root: PathBuf, workspaces: Vec<ProjectWorkspace>) -> ServerWorldState {
pub fn new(folder_roots: Vec<PathBuf>, workspaces: Vec<ProjectWorkspace>) -> ServerWorldState {
let mut change = AnalysisChange::new();
let mut roots = Vec::new();
roots.push(IncludeRustFiles::member(root.clone()));
roots.extend(folder_roots.iter().cloned().map(IncludeRustFiles::member));
for ws in workspaces.iter() {
roots.extend(IncludeRustFiles::from_roots(ws.to_roots()));
}
let (mut vfs, roots) = Vfs::new(roots);
let roots_to_scan = roots.len();
for r in roots {
let is_local = vfs.root2path(r).starts_with(&root);
let (mut vfs, vfs_roots) = Vfs::new(roots);
let roots_to_scan = vfs_roots.len();
for r in vfs_roots {
let vfs_root_path = vfs.root2path(r);
let is_local = folder_roots.iter().any(|it| vfs_root_path.starts_with(it));
change.add_root(SourceRootId(r.0.into()), is_local);
}
@ -68,7 +69,7 @@ impl ServerWorldState {
analysis_host.apply_change(change);
ServerWorldState {
roots_to_scan,
root,
roots: folder_roots,
workspaces: Arc::new(workspaces),
analysis_host,
vfs: Arc::new(RwLock::new(vfs)),
@ -90,7 +91,8 @@ impl ServerWorldState {
match c {
VfsChange::AddRoot { root, files } => {
let root_path = self.vfs.read().root2path(root);
if root_path.starts_with(&self.root) {
let is_local = self.roots.iter().any(|r| root_path.starts_with(r));
if is_local {
self.roots_to_scan -= 1;
for (file, path, text) in files {
change.add_file(
@ -193,4 +195,9 @@ impl ServerWorld {
res.push_str(&self.analysis.status());
res
}
pub fn workspace_root_for(&self, file_id: FileId) -> Option<&Path> {
let path = self.vfs.read().file2path(VfsFile(file_id.0.into()));
self.workspaces.iter().find_map(|ws| ws.workspace_root_for(&path))
}
}

View file

@ -14,7 +14,7 @@ use ra_lsp_server::req::{
use serde_json::json;
use tempfile::TempDir;
use crate::support::{project, project_with_tmpdir};
use crate::support::{project, Project};
const LOG: &'static str = "";
@ -62,6 +62,7 @@ fn foo() {
"args": [ "test", "--", "foo", "--nocapture" ],
"bin": "cargo",
"env": { "RUST_BACKTRACE": "short" },
"cwd": null,
"label": "test foo",
"range": {
"end": { "character": 1, "line": 2 },
@ -75,6 +76,7 @@ fn foo() {
],
"bin": "cargo",
"env": {},
"cwd": null,
"label": "cargo check --all",
"range": {
"end": {
@ -93,25 +95,34 @@ fn foo() {
#[test]
fn test_runnables_project() {
let server = project(
r#"
//- Cargo.toml
let code = r#"
//- foo/Cargo.toml
[package]
name = "foo"
version = "0.0.0"
//- src/lib.rs
//- foo/src/lib.rs
pub fn foo() {}
//- tests/spam.rs
//- foo/tests/spam.rs
#[test]
fn test_eggs() {}
"#,
);
//- bar/Cargo.toml
[package]
name = "bar"
version = "0.0.0"
//- bar/src/main.rs
fn main() {}
"#;
let server = Project::with_fixture(code).root("foo").root("bar").server();
server.wait_until_workspace_is_loaded();
server.request::<Runnables>(
RunnablesParams {
text_document: server.doc_id("tests/spam.rs"),
text_document: server.doc_id("foo/tests/spam.rs"),
position: None,
},
json!([
@ -123,7 +134,8 @@ fn test_eggs() {}
"range": {
"end": { "character": 17, "line": 1 },
"start": { "character": 0, "line": 0 }
}
},
"cwd": server.path().join("foo")
},
{
"args": [
@ -135,6 +147,7 @@ fn test_eggs() {}
],
"bin": "cargo",
"env": {},
"cwd": server.path().join("foo"),
"label": "cargo check -p foo",
"range": {
"end": {
@ -283,7 +296,9 @@ fn main() {{}}
"#,
PROJECT = project.to_string(),
);
let server = project_with_tmpdir(tmp_dir, &code);
let server = Project::with_fixture(&code).tmp_dir(tmp_dir).server();
server.wait_until_workspace_is_loaded();
let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None };
server.request::<CodeActionRequest>(

View file

@ -1,7 +1,7 @@
use std::{
cell::{Cell, RefCell},
fs,
path::PathBuf,
path::{Path, PathBuf},
sync::Once,
time::Duration,
};
@ -26,26 +26,51 @@ use ra_lsp_server::{
InitializationOptions,
};
pub fn project(fixture: &str) -> Server {
let tmp_dir = TempDir::new().unwrap();
project_with_tmpdir(tmp_dir, fixture)
pub struct Project<'a> {
fixture: &'a str,
tmp_dir: Option<TempDir>,
roots: Vec<PathBuf>,
}
pub fn project_with_tmpdir(tmp_dir: TempDir, fixture: &str) -> Server {
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = Logger::with_env_or_str(crate::LOG).start().unwrap();
});
let mut paths = vec![];
for entry in parse_fixture(fixture) {
let path = tmp_dir.path().join(entry.meta);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
paths.push((path, entry.text));
impl<'a> Project<'a> {
pub fn with_fixture(fixture: &str) -> Project {
Project { fixture, tmp_dir: None, roots: vec![] }
}
Server::new(tmp_dir, paths)
pub fn tmp_dir(mut self, tmp_dir: TempDir) -> Project<'a> {
self.tmp_dir = Some(tmp_dir);
self
}
pub fn root(mut self, path: &str) -> Project<'a> {
self.roots.push(path.into());
self
}
pub fn server(self) -> Server {
let tmp_dir = self.tmp_dir.unwrap_or_else(|| TempDir::new().unwrap());
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = Logger::with_env_or_str(crate::LOG).start().unwrap();
});
let mut paths = vec![];
for entry in parse_fixture(self.fixture) {
let path = tmp_dir.path().join(entry.meta);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
paths.push((path, entry.text));
}
let roots = self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect();
Server::new(tmp_dir, roots, paths)
}
}
pub fn project(fixture: &str) -> Server {
Project::with_fixture(fixture).server()
}
pub struct Server {
@ -56,14 +81,17 @@ pub struct Server {
}
impl Server {
fn new(dir: TempDir, files: Vec<(PathBuf, String)>) -> Server {
fn new(dir: TempDir, roots: Vec<PathBuf>, files: Vec<(PathBuf, String)>) -> Server {
let path = dir.path().to_path_buf();
let roots = if roots.is_empty() { vec![path] } else { roots };
let worker = Worker::<RawMessage, RawMessage>::spawn(
"test server",
128,
move |mut msg_receiver, mut msg_sender| {
main_loop(
path,
roots,
InitializationOptions::default(),
&mut msg_receiver,
&mut msg_sender,
@ -177,6 +205,10 @@ impl Server {
fn send_notification(&self, not: RawNotification) {
self.worker.as_ref().unwrap().sender().send(RawMessage::Notification(not)).unwrap();
}
pub fn path(&self) -> &Path {
self.dir.path()
}
}
impl Drop for Server {

View file

@ -19,6 +19,7 @@ use crate::Result;
pub struct CargoWorkspace {
packages: Arena<Package, PackageData>,
targets: Arena<Target, TargetData>,
pub(crate) workspace_root: PathBuf,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@ -165,7 +166,7 @@ impl CargoWorkspace {
}
}
Ok(CargoWorkspace { packages, targets })
Ok(CargoWorkspace { packages, targets, workspace_root: meta.workspace_root })
}
pub fn packages<'a>(&'a self) -> impl Iterator<Item = Package> + 'a {

View file

@ -255,6 +255,18 @@ impl ProjectWorkspace {
}
crate_graph
}
pub fn workspace_root_for(&self, path: &Path) -> Option<&Path> {
match self {
ProjectWorkspace::Cargo { cargo, .. } => {
Some(cargo.workspace_root.as_ref()).filter(|root| path.starts_with(root))
}
ProjectWorkspace::Json { project: JsonProject { roots, .. } } => roots
.iter()
.find(|root| path.starts_with(&root.path))
.map(|root| root.path.as_ref()),
}
}
}
fn find_rust_project_json(path: &Path) -> Option<PathBuf> {

View file

@ -17,6 +17,7 @@ interface Runnable {
bin: string;
args: string[];
env: { [index: string]: string };
cwd?: string;
}
class RunnableQuickPick implements vscode.QuickPickItem {
@ -49,7 +50,7 @@ function createTask(spec: Runnable): vscode.Task {
};
const execOption: vscode.ShellExecutionOptions = {
cwd: '.',
cwd: spec.cwd || '.',
env: definition.env
};
const exec = new vscode.ShellExecution(

View file

@ -17,13 +17,6 @@ export class Server {
let folder: string = '.';
if (workspace.workspaceFolders !== undefined) {
folder = workspace.workspaceFolders[0].uri.fsPath.toString();
if (workspace.workspaceFolders.length > 1) {
// Tell the user that we do not support multi-root workspaces yet
window.showWarningMessage(
'Multi-root workspaces are not currently supported'
);
}
}
const run: lc.Executable = {