Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 110 additions & 121 deletions crates/ra_project_model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ mod json_project;
mod sysroot;

use std::{
error::Error,
fs::{read_dir, File, ReadDir},
io::BufReader,
io::{self, BufReader},
path::{Path, PathBuf},
process::Command,
};
Expand All @@ -25,25 +24,6 @@ pub use crate::{
};
pub use ra_proc_macro::ProcMacroClient;

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct CargoTomlNotFoundError {
pub searched_at: PathBuf,
pub reason: String,
}

impl std::fmt::Display for CargoTomlNotFoundError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
fmt,
"can't find Cargo.toml at {}, due to {}",
self.searched_at.display(),
self.reason
)
}
}

impl Error for CargoTomlNotFoundError {}

#[derive(Debug, Clone)]
pub enum ProjectWorkspace {
/// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`.
Expand Down Expand Up @@ -77,31 +57,119 @@ impl PackageRoot {
}
}

impl ProjectWorkspace {
pub fn discover(path: &Path, cargo_features: &CargoConfig) -> Result<ProjectWorkspace> {
ProjectWorkspace::discover_with_sysroot(path, true, cargo_features)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProjectRoot {
ProjectJson(PathBuf),
CargoToml(PathBuf),
}

impl ProjectRoot {
pub fn from_manifest_file(path: PathBuf) -> Result<ProjectRoot> {
if path.ends_with("rust-project.json") {
return Ok(ProjectRoot::ProjectJson(path));
}
if path.ends_with("Cargo.toml") {
return Ok(ProjectRoot::CargoToml(path));
}
bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
}

pub fn discover_with_sysroot(
path: &Path,
with_sysroot: bool,
pub fn discover_single(path: &Path) -> Result<ProjectRoot> {
let mut candidates = ProjectRoot::discover(path)?;
let res = match candidates.pop() {
None => bail!("no projects"),
Some(it) => it,
};

if !candidates.is_empty() {
bail!("more than one project")
}
Ok(res)
}

pub fn discover(path: &Path) -> io::Result<Vec<ProjectRoot>> {
if let Some(project_json) = find_rust_project_json(path) {
return Ok(vec![ProjectRoot::ProjectJson(project_json)]);
}
return find_cargo_toml(path)
.map(|paths| paths.into_iter().map(ProjectRoot::CargoToml).collect());

fn find_rust_project_json(path: &Path) -> Option<PathBuf> {
if path.ends_with("rust-project.json") {
return Some(path.to_path_buf());
}

let mut curr = Some(path);
while let Some(path) = curr {
let candidate = path.join("rust-project.json");
if candidate.exists() {
return Some(candidate);
}
curr = path.parent();
}

None
}

fn find_cargo_toml(path: &Path) -> io::Result<Vec<PathBuf>> {
if path.ends_with("Cargo.toml") {
return Ok(vec![path.to_path_buf()]);
}

if let Some(p) = find_cargo_toml_in_parent_dir(path) {
return Ok(vec![p]);
}

let entities = read_dir(path)?;
Ok(find_cargo_toml_in_child_dir(entities))
}

fn find_cargo_toml_in_parent_dir(path: &Path) -> Option<PathBuf> {
let mut curr = Some(path);
while let Some(path) = curr {
let candidate = path.join("Cargo.toml");
if candidate.exists() {
return Some(candidate);
}
curr = path.parent();
}

None
}

fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<PathBuf> {
// Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
let mut valid_canditates = vec![];
for entity in entities.filter_map(Result::ok) {
let candidate = entity.path().join("Cargo.toml");
if candidate.exists() {
valid_canditates.push(candidate)
}
}
valid_canditates
}
}
}

impl ProjectWorkspace {
pub fn load(
root: ProjectRoot,
cargo_features: &CargoConfig,
with_sysroot: bool,
) -> Result<ProjectWorkspace> {
match find_rust_project_json(path) {
Some(json_path) => {
let file = File::open(&json_path)
.with_context(|| format!("Failed to open json file {}", json_path.display()))?;
let res = match root {
ProjectRoot::ProjectJson(project_json) => {
let file = File::open(&project_json).with_context(|| {
format!("Failed to open json file {}", project_json.display())
})?;
let reader = BufReader::new(file);
Ok(ProjectWorkspace::Json {
ProjectWorkspace::Json {
project: from_reader(reader).with_context(|| {
format!("Failed to deserialize json file {}", json_path.display())
format!("Failed to deserialize json file {}", project_json.display())
})?,
})
}
}
None => {
let cargo_toml = find_cargo_toml(path).with_context(|| {
format!("Failed to find Cargo.toml for path {}", path.display())
})?;
ProjectRoot::CargoToml(cargo_toml) => {
let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_features)
.with_context(|| {
format!(
Expand All @@ -119,9 +187,11 @@ impl ProjectWorkspace {
} else {
Sysroot::default()
};
Ok(ProjectWorkspace::Cargo { cargo, sysroot })
ProjectWorkspace::Cargo { cargo, sysroot }
}
}
};

Ok(res)
}

/// Returns the roots for the current `ProjectWorkspace`
Expand Down Expand Up @@ -469,87 +539,6 @@ impl ProjectWorkspace {
}
}

fn find_rust_project_json(path: &Path) -> Option<PathBuf> {
if path.ends_with("rust-project.json") {
return Some(path.to_path_buf());
}

let mut curr = Some(path);
while let Some(path) = curr {
let candidate = path.join("rust-project.json");
if candidate.exists() {
return Some(candidate);
}
curr = path.parent();
}

None
}

fn find_cargo_toml_in_parent_dir(path: &Path) -> Option<PathBuf> {
let mut curr = Some(path);
while let Some(path) = curr {
let candidate = path.join("Cargo.toml");
if candidate.exists() {
return Some(candidate);
}
curr = path.parent();
}

None
}

fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<PathBuf> {
// Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
let mut valid_canditates = vec![];
for entity in entities.filter_map(Result::ok) {
let candidate = entity.path().join("Cargo.toml");
if candidate.exists() {
valid_canditates.push(candidate)
}
}
valid_canditates
}

fn find_cargo_toml(path: &Path) -> Result<PathBuf> {
if path.ends_with("Cargo.toml") {
return Ok(path.to_path_buf());
}

if let Some(p) = find_cargo_toml_in_parent_dir(path) {
return Ok(p);
}

let entities = match read_dir(path) {
Ok(entities) => entities,
Err(e) => {
return Err(CargoTomlNotFoundError {
searched_at: path.to_path_buf(),
reason: format!("file system error: {}", e),
}
.into());
}
};

let mut valid_canditates = find_cargo_toml_in_child_dir(entities);
match valid_canditates.len() {
1 => Ok(valid_canditates.remove(0)),
0 => Err(CargoTomlNotFoundError {
searched_at: path.to_path_buf(),
reason: "no Cargo.toml file found".to_string(),
}
.into()),
_ => Err(CargoTomlNotFoundError {
searched_at: path.to_path_buf(),
reason: format!(
"multiple equally valid Cargo.toml files found: {:?}",
valid_canditates
),
}
.into()),
}
}

pub fn get_rustc_cfg_options() -> CfgOptions {
let mut cfg_options = CfgOptions::default();

Expand Down
8 changes: 5 additions & 3 deletions crates/rust-analyzer/src/cli/load_cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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, ProjectWorkspace,
get_rustc_cfg_options, CargoConfig, PackageRoot, ProcMacroClient, ProjectRoot, ProjectWorkspace,
};
use ra_vfs::{RootEntry, Vfs, VfsChange, VfsTask, Watch};
use rustc_hash::{FxHashMap, FxHashSet};
Expand All @@ -27,9 +27,11 @@ pub(crate) fn load_cargo(
load_out_dirs_from_check: bool,
) -> Result<(AnalysisHost, FxHashMap<SourceRootId, PackageRoot>)> {
let root = std::env::current_dir()?.join(root);
let ws = ProjectWorkspace::discover(
root.as_ref(),
let root = ProjectRoot::discover_single(&root)?;
let ws = ProjectWorkspace::load(
root,
&CargoConfig { load_out_dirs_from_check, ..Default::default() },
true,
)?;

let mut extern_dirs = FxHashSet::default();
Expand Down
60 changes: 35 additions & 25 deletions crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::{
};

use crossbeam_channel::{never, select, unbounded, RecvError, Sender};
use itertools::Itertools;
use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
use lsp_types::{
NumberOrString, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams,
Expand Down Expand Up @@ -88,37 +89,46 @@ pub fn main_loop(ws_roots: Vec<PathBuf>, config: Config, connection: Connection)

let mut loop_state = LoopState::default();
let mut world_state = {
// FIXME: support dynamic workspace loading.
let workspaces = {
let mut loaded_workspaces = Vec::new();
for ws_root in &ws_roots {
let workspace = ra_project_model::ProjectWorkspace::discover_with_sysroot(
ws_root.as_path(),
config.with_sysroot,
&config.cargo,
);
match workspace {
Ok(workspace) => loaded_workspaces.push(workspace),
Err(e) => {
log::error!("loading workspace failed: {:?}", e);

if let Some(ra_project_model::CargoTomlNotFoundError { .. }) =
e.downcast_ref()
{
if !config.notifications.cargo_toml_not_found {
continue;
}
}
// FIXME: support dynamic workspace loading.
let mut visited = FxHashSet::default();
let project_roots = ws_roots
.iter()
.filter_map(|it| ra_project_model::ProjectRoot::discover(it).ok())
.flatten()
.filter(|it| visited.insert(it.clone()))
.collect::<Vec<_>>();

if project_roots.is_empty() && config.notifications.cargo_toml_not_found {
show_message(
req::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()))
),
&connection.sender,
);
};

project_roots
.into_iter()
.filter_map(|root| {
ra_project_model::ProjectWorkspace::load(
root,
&config.cargo,
config.with_sysroot,
)
.map_err(|err| {
log::error!("failed to load workspace: {:#}", err);
show_message(
req::MessageType::Error,
format!("rust-analyzer failed to load workspace: {:?}", e),
format!("rust-analyzer failed to load workspace: {:#}", err),
&connection.sender,
);
}
}
}
loaded_workspaces
})
.ok()
})
.collect::<Vec<_>>()
};

let globs = config
Expand Down