Repository: rust-analyzer/rust-analyzer
Pull request #4726 (2020-06-03 10:26:22)

Groundwork for specifying the set of projects via config


bors r+
🤖


Patch:
diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs
index 9b30bef8d3..a996126902 100644
--- a/crates/ra_project_model/src/lib.rs
+++ b/crates/ra_project_model/src/lib.rs
@@ -14,7 +14,7 @@ use std::{
 use anyhow::{bail, Context, Result};
 use ra_cfg::CfgOptions;
 use ra_db::{CrateGraph, CrateName, Edition, Env, ExternSource, ExternSourceId, FileId};
-use rustc_hash::FxHashMap;
+use rustc_hash::{FxHashMap, FxHashSet};
 use serde_json::from_reader;
 
 pub use crate::{
@@ -57,25 +57,25 @@ impl PackageRoot {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum ProjectRoot {
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
+pub enum ProjectManifest {
     ProjectJson(PathBuf),
     CargoToml(PathBuf),
 }
 
-impl ProjectRoot {
-    pub fn from_manifest_file(path: PathBuf) -> Result<ProjectRoot> {
+impl ProjectManifest {
+    pub fn from_manifest_file(path: PathBuf) -> Result<ProjectManifest> {
         if path.ends_with("rust-project.json") {
-            return Ok(ProjectRoot::ProjectJson(path));
+            return Ok(ProjectManifest::ProjectJson(path));
         }
         if path.ends_with("Cargo.toml") {
-            return Ok(ProjectRoot::CargoToml(path));
+            return Ok(ProjectManifest::CargoToml(path));
         }
         bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
     }
 
-    pub fn discover_single(path: &Path) -> Result<ProjectRoot> {
-        let mut candidates = ProjectRoot::discover(path)?;
+    pub fn discover_single(path: &Path) -> Result<ProjectManifest> {
+        let mut candidates = ProjectManifest::discover(path)?;
         let res = match candidates.pop() {
             None => bail!("no projects"),
             Some(it) => it,
@@ -87,12 +87,12 @@ impl ProjectRoot {
         Ok(res)
     }
 
-    pub fn discover(path: &Path) -> io::Result<Vec<ProjectRoot>> {
+    pub fn discover(path: &Path) -> io::Result<Vec<ProjectManifest>> {
         if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
-            return Ok(vec![ProjectRoot::ProjectJson(project_json)]);
+            return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
         }
         return find_cargo_toml(path)
-            .map(|paths| paths.into_iter().map(ProjectRoot::CargoToml).collect());
+            .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
 
         fn find_cargo_toml(path: &Path) -> io::Result<Vec<PathBuf>> {
             match find_in_parent_dirs(path, "Cargo.toml") {
@@ -128,16 +128,28 @@ impl ProjectRoot {
                 .collect()
         }
     }
+
+    pub fn discover_all(paths: &[impl AsRef<Path>]) -> Vec<ProjectManifest> {
+        let mut res = paths
+            .iter()
+            .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
+            .flatten()
+            .collect::<FxHashSet<_>>()
+            .into_iter()
+            .collect::<Vec<_>>();
+        res.sort();
+        res
+    }
 }
 
 impl ProjectWorkspace {
     pub fn load(
-        root: ProjectRoot,
+        root: ProjectManifest,
         cargo_features: &CargoConfig,
         with_sysroot: bool,
     ) -> Result<ProjectWorkspace> {
         let res = match root {
-            ProjectRoot::ProjectJson(project_json) => {
+            ProjectManifest::ProjectJson(project_json) => {
                 let file = File::open(&project_json).with_context(|| {
                     format!("Failed to open json file {}", project_json.display())
                 })?;
@@ -148,7 +160,7 @@ impl ProjectWorkspace {
                     })?,
                 }
             }
-            ProjectRoot::CargoToml(cargo_toml) => {
+            ProjectManifest::CargoToml(cargo_toml) => {
                 let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_features)
                     .with_context(|| {
                         format!(
diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs
index e82fd57de8..8d071ab1ca 100644
--- a/crates/rust-analyzer/src/bin/main.rs
+++ b/crates/rust-analyzer/src/bin/main.rs
@@ -4,9 +4,14 @@
 mod args;
 
 use lsp_server::Connection;
-use rust_analyzer::{cli, config::Config, from_json, Result};
+use rust_analyzer::{
+    cli,
+    config::{Config, LinkedProject},
+    from_json, Result,
+};
 
 use crate::args::HelpPrinted;
+use ra_project_model::ProjectManifest;
 
 fn main() -> Result<()> {
     setup_logging()?;
@@ -97,17 +102,6 @@ fn run_server() -> Result<()> {
         log::info!("Client '{}' {}", client_info.name, client_info.version.unwrap_or_default());
     }
 
-    let cwd = std::env::current_dir()?;
-    let root = initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd);
-
-    let workspace_roots = initialize_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 config = {
         let mut config = Config::default();
         if let Some(value) = &initialize_params.initialization_options {
@@ -115,10 +109,31 @@ fn run_server() -> Result<()> {
         }
         config.update_caps(&initialize_params.capabilities);
 
+        if config.linked_projects.is_empty() {
+            let cwd = std::env::current_dir()?;
+            let root =
+                initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd);
+            let workspace_roots = initialize_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]);
+
+            config.linked_projects = ProjectManifest::discover_all(&workspace_roots)
+                .into_iter()
+                .map(LinkedProject::from)
+                .collect();
+        }
+
         config
     };
 
-    rust_analyzer::main_loop(workspace_roots, config, connection)?;
+    rust_analyzer::main_loop(config, connection)?;
 
     log::info!("shutting down IO...");
     io_threads.join()?;
diff --git a/crates/rust-analyzer/src/cli/load_cargo.rs b/crates/rust-analyzer/src/cli/load_cargo.rs
index 8eaf75ff61..c7e86fe0c4 100644
--- a/crates/rust-analyzer/src/cli/load_cargo.rs
+++ b/crates/rust-analyzer/src/cli/load_cargo.rs
@@ -8,7 +8,8 @@ use crossbeam_channel::{unbounded, Receiver};
 use ra_db::{ExternSourceId, FileId, SourceRootId};
 use ra_ide::{AnalysisChange, AnalysisHost};
 use ra_project_model::{
-    get_rustc_cfg_options, CargoConfig, PackageRoot, ProcMacroClient, ProjectRoot, ProjectWorkspace,
+    get_rustc_cfg_options, CargoConfig, PackageRoot, ProcMacroClient, ProjectManifest,
+    ProjectWorkspace,
 };
 use ra_vfs::{RootEntry, Vfs, VfsChange, VfsTask, Watch};
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -28,7 +29,7 @@ pub fn load_cargo(
     with_proc_macro: bool,
 ) -> Result<(AnalysisHost, FxHashMap<SourceRootId, PackageRoot>)> {
     let root = std::env::current_dir()?.join(root);
-    let root = ProjectRoot::discover_single(&root)?;
+    let root = ProjectManifest::discover_single(&root)?;
     let ws = ProjectWorkspace::load(
         root,
         &CargoConfig { load_out_dirs_from_check, ..Default::default() },
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 9c6e369d2e..761bc9c2d9 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -12,14 +12,13 @@ use std::{ffi::OsString, path::PathBuf};
 use lsp_types::ClientCapabilities;
 use ra_flycheck::FlycheckConfig;
 use ra_ide::{AssistConfig, CompletionConfig, InlayHintsConfig};
-use ra_project_model::CargoConfig;
+use ra_project_model::{CargoConfig, JsonProject, ProjectManifest};
 use serde::Deserialize;
 
 #[derive(Debug, Clone)]
 pub struct Config {
     pub client_caps: ClientCapsConfig,
 
-    pub with_sysroot: bool,
     pub publish_diagnostics: bool,
     pub lru_capacity: Option<usize>,
     pub proc_macro_srv: Option<(PathBuf, Vec<OsString>)>,
@@ -35,6 +34,27 @@ pub struct Config {
     pub assist: AssistConfig,
     pub call_info_full: bool,
     pub lens: LensConfig,
+
+    pub with_sysroot: bool,
+    pub linked_projects: Vec<LinkedProject>,
+}
+
+#[derive(Debug, Clone)]
+pub enum LinkedProject {
+    ProjectManifest(ProjectManifest),
+    JsonProject(JsonProject),
+}
+
+impl From<ProjectManifest> for LinkedProject {
+    fn from(v: ProjectManifest) -> Self {
+        LinkedProject::ProjectManifest(v)
+    }
+}
+
+impl From<JsonProject> for LinkedProject {
+    fn from(v: JsonProject) -> Self {
+        LinkedProject::JsonProject(v)
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -141,6 +161,7 @@ impl Default for Config {
             assist: AssistConfig::default(),
             call_info_full: true,
             lens: LensConfig::default(),
+            linked_projects: Vec::new(),
         }
     }
 }
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 35f2d7001e..d901f21d7e 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -12,13 +12,11 @@ use std::{
     fmt,
     ops::Range,
     panic,
-    path::PathBuf,
     sync::Arc,
     time::{Duration, Instant},
 };
 
 use crossbeam_channel::{never, select, unbounded, RecvError, Sender};
-use itertools::Itertools;
 use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
 use lsp_types::{
     DidChangeTextDocumentParams, NumberOrString, TextDocumentContentChangeEvent, WorkDoneProgress,
@@ -36,7 +34,7 @@ use serde::{de::DeserializeOwned, Serialize};
 use threadpool::ThreadPool;
 
 use crate::{
-    config::{Config, FilesWatcher},
+    config::{Config, FilesWatcher, LinkedProject},
     diagnostics::{to_proto::url_from_path_with_drive_lowercasing, DiagnosticTask},
     from_proto,
     global_state::{GlobalState, GlobalStateSnapshot},
@@ -70,7 +68,7 @@ impl fmt::Display for LspError {
 
 impl Error for LspError {}
 
-pub fn main_loop(ws_roots: Vec<PathBuf>, config: Config, connection: Connection) -> Result<()> {
+pub fn main_loop(config: Config, connection: Connection) -> Result<()> {
     log::info!("initial config: {:#?}", config);
 
     // Windows scheduler implements priority boosts: if thread waits for an
@@ -95,29 +93,24 @@ pub fn main_loop(ws_roots: Vec<PathBuf>, config: Config, connection: Connection)
     let mut loop_state = LoopState::default();
     let mut global_state = {
         let workspaces = {
-            // FIXME: support dynamic workspace loading.
-            let project_roots: FxHashSet<_> = ws_roots
-                .iter()
-                .filter_map(|it| ra_project_model::ProjectRoot::discover(it).ok())
-                .flatten()
-                .collect();
-
-            if project_roots.is_empty() && config.notifications.cargo_toml_not_found {
+            if config.linked_projects.is_empty() && config.notifications.cargo_toml_not_found {
                 show_message(
                     lsp_types::MessageType::Error,
-                    format!(
-                        "rust-analyzer failed to discover workspace, no Cargo.toml found, dirs searched: {}",
-                        ws_roots.iter().format_with(", ", |it, f| f(&it.display()))
-                    ),
+                    "rust-analyzer failed to discover workspace".to_string(),
                     &connection.sender,
                 );
             };
 
-            project_roots
-                .into_iter()
+            config
+                .linked_projects
+                .iter()
+                .filter_map(|project| match project {
+                    LinkedProject::ProjectManifest(it) => Some(it),
+                    LinkedProject::JsonProject(_) => None,
+                })
                 .filter_map(|root| {
                     ra_project_model::ProjectWorkspace::load(
-                        root,
+                        root.clone(),
                         &config.cargo,
                         config.with_sysroot,
                     )


Test patch:
diff --git a/crates/rust-analyzer/tests/heavy_tests/support.rs b/crates/rust-analyzer/tests/heavy_tests/support.rs
index 66a6f4d541..30d03b622b 100644
--- a/crates/rust-analyzer/tests/heavy_tests/support.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/support.rs
@@ -19,8 +19,9 @@ use serde_json::{to_string_pretty, Value};
 use tempfile::TempDir;
 use test_utils::{find_mismatch, parse_fixture};
 
+use ra_project_model::ProjectManifest;
 use rust_analyzer::{
-    config::{ClientCapsConfig, Config},
+    config::{ClientCapsConfig, Config, LinkedProject},
     main_loop,
 };
 
@@ -42,7 +43,7 @@ impl<'a> Project<'a> {
         self
     }
 
-    pub fn root(mut self, path: &str) -> Project<'a> {
+    pub(crate) fn root(mut self, path: &str) -> Project<'a> {
         self.roots.push(path.into());
         self
     }
@@ -74,7 +75,16 @@ impl<'a> Project<'a> {
             paths.push((path, entry.text));
         }
 
-        let roots = self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect();
+        let mut roots =
+            self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect::<Vec<_>>();
+        if roots.is_empty() {
+            roots.push(tmp_dir.path().to_path_buf());
+        }
+        let linked_projects = roots
+            .into_iter()
+            .map(|it| ProjectManifest::discover_single(&it).unwrap())
+            .map(LinkedProject::from)
+            .collect::<Vec<_>>();
 
         let mut config = Config {
             client_caps: ClientCapsConfig {
@@ -84,6 +94,7 @@ impl<'a> Project<'a> {
                 ..Default::default()
             },
             with_sysroot: self.with_sysroot,
+            linked_projects,
             ..Config::default()
         };
 
@@ -91,7 +102,7 @@ impl<'a> Project<'a> {
             f(&mut config)
         }
 
-        Server::new(tmp_dir, config, roots, paths)
+        Server::new(tmp_dir, config, paths)
     }
 }
 
@@ -109,20 +120,12 @@ pub struct Server {
 }
 
 impl Server {
-    fn new(
-        dir: TempDir,
-        config: Config,
-        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 };
+    fn new(dir: TempDir, config: Config, files: Vec<(PathBuf, String)>) -> Server {
         let (connection, client) = Connection::memory();
 
         let _thread = jod_thread::Builder::new()
             .name("test server".to_string())
-            .spawn(move || main_loop(roots, config, connection).unwrap())
+            .spawn(move || main_loop(config, connection).unwrap())
             .expect("failed to spawn a thread");
 
         let res =