Merge #1137
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:
commit
88be6f3217
11 changed files with 145 additions and 64 deletions
|
@ -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()?;
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Reference in a new issue