From e6c61d5072e600372ba4a38ad8893af37aaa77e6 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Wed, 24 Jun 2020 15:52:07 +0200 Subject: Cleanup project.json deserialization --- Cargo.lock | 1 + crates/ra_project_model/Cargo.toml | 1 + crates/ra_project_model/src/cargo_workspace.rs | 2 +- crates/ra_project_model/src/lib.rs | 77 +++++--------- crates/ra_project_model/src/project_json.rs | 140 ++++++++++++++----------- crates/rust-analyzer/src/cargo_target_spec.rs | 5 +- crates/rust-analyzer/src/cli/load_cargo.rs | 11 +- crates/rust-analyzer/src/config.rs | 12 +-- crates/rust-analyzer/src/global_state.rs | 13 ++- crates/rust-analyzer/src/main_loop.rs | 5 +- crates/rust-analyzer/src/main_loop/handlers.rs | 2 +- crates/rust-analyzer/src/to_proto.rs | 2 +- crates/vfs-notify/src/lib.rs | 2 +- crates/vfs/src/loader.rs | 4 +- docs/user/manual.adoc | 2 + 15 files changed, 138 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91a932549..7bd2144a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,6 +1183,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "stdx", ] [[package]] diff --git a/crates/ra_project_model/Cargo.toml b/crates/ra_project_model/Cargo.toml index 818947014..b1b44dcf7 100644 --- a/crates/ra_project_model/Cargo.toml +++ b/crates/ra_project_model/Cargo.toml @@ -19,6 +19,7 @@ ra_db = { path = "../ra_db" } ra_toolchain = { path = "../ra_toolchain" } ra_proc_macro = { path = "../ra_proc_macro" } paths = { path = "../paths" } +stdx = { path = "../stdx" } serde = { version = "1.0.106", features = ["derive"] } serde_json = "1.0.48" diff --git a/crates/ra_project_model/src/cargo_workspace.rs b/crates/ra_project_model/src/cargo_workspace.rs index 8ce63ab3c..3b124020d 100644 --- a/crates/ra_project_model/src/cargo_workspace.rs +++ b/crates/ra_project_model/src/cargo_workspace.rs @@ -260,7 +260,7 @@ impl CargoWorkspace { .copied() } - pub fn workspace_root(&self) -> &Path { + pub fn workspace_root(&self) -> &AbsPath { &self.workspace_root } diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs index 7e8e00df8..8b85b4831 100644 --- a/crates/ra_project_model/src/lib.rs +++ b/crates/ra_project_model/src/lib.rs @@ -5,8 +5,8 @@ mod project_json; mod sysroot; use std::{ - fs::{read_dir, File, ReadDir}, - io::{self, BufReader}, + fs::{self, read_dir, ReadDir}, + io, path::Path, process::{Command, Output}, }; @@ -14,13 +14,12 @@ use std::{ use anyhow::{bail, Context, Result}; use paths::{AbsPath, AbsPathBuf}; use ra_cfg::CfgOptions; -use ra_db::{CrateGraph, CrateName, Edition, Env, FileId}; +use ra_db::{CrateGraph, CrateId, CrateName, Edition, Env, FileId}; use rustc_hash::{FxHashMap, FxHashSet}; -use serde_json::from_reader; pub use crate::{ cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind}, - project_json::ProjectJson, + project_json::{ProjectJson, ProjectJsonData}, sysroot::Sysroot, }; pub use ra_proc_macro::ProcMacroClient; @@ -30,7 +29,7 @@ pub enum ProjectWorkspace { /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`. Cargo { cargo: CargoWorkspace, sysroot: Sysroot }, /// Project workspace was manually specified using a `rust-project.json` file. - Json { project: ProjectJson, project_location: AbsPathBuf }, + Json { project: ProjectJson }, } /// `PackageRoot` describes a package root folder. @@ -156,17 +155,15 @@ impl ProjectWorkspace { ) -> Result { let res = match manifest { ProjectManifest::ProjectJson(project_json) => { - let file = File::open(&project_json).with_context(|| { - format!("Failed to open json file {}", project_json.display()) + let file = fs::read_to_string(&project_json).with_context(|| { + format!("Failed to read json file {}", project_json.display()) + })?; + let data = serde_json::from_str(&file).with_context(|| { + format!("Failed to deserialize json file {}", project_json.display()) })?; - let reader = BufReader::new(file); let project_location = project_json.parent().unwrap().to_path_buf(); - ProjectWorkspace::Json { - project: from_reader(reader).with_context(|| { - format!("Failed to deserialize json file {}", project_json.display()) - })?, - project_location, - } + let project = ProjectJson::new(&project_location, data); + ProjectWorkspace::Json { project } } ProjectManifest::CargoToml(cargo_toml) => { let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_features) @@ -198,11 +195,9 @@ impl ProjectWorkspace { /// the root is a member of the current workspace pub fn to_roots(&self) -> Vec { match self { - ProjectWorkspace::Json { project, project_location } => project - .roots - .iter() - .map(|r| PackageRoot::new_member(project_location.join(&r.path))) - .collect(), + ProjectWorkspace::Json { project } => { + project.roots.iter().map(|r| PackageRoot::new_member(r.path.clone())).collect() + } ProjectWorkspace::Cargo { cargo, sysroot } => cargo .packages() .map(|pkg| PackageRoot { @@ -219,11 +214,11 @@ impl ProjectWorkspace { pub fn proc_macro_dylib_paths(&self) -> Vec { match self { - ProjectWorkspace::Json { project, project_location } => project + ProjectWorkspace::Json { project } => project .crates .iter() .filter_map(|krate| krate.proc_macro_dylib_path.as_ref()) - .map(|it| project_location.join(it)) + .cloned() .collect(), ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => cargo .packages() @@ -246,36 +241,18 @@ impl ProjectWorkspace { &self, target: Option<&str>, proc_macro_client: &ProcMacroClient, - load: &mut dyn FnMut(&Path) -> Option, + load: &mut dyn FnMut(&AbsPath) -> Option, ) -> CrateGraph { let mut crate_graph = CrateGraph::default(); match self { - ProjectWorkspace::Json { project, project_location } => { + ProjectWorkspace::Json { project } => { let crates: FxHashMap<_, _> = project .crates .iter() .enumerate() .filter_map(|(seq_index, krate)| { - let file_path = project_location.join(&krate.root_module); + let file_path = &krate.root_module; let file_id = load(&file_path)?; - let edition = match krate.edition { - project_json::Edition::Edition2015 => Edition::Edition2015, - project_json::Edition::Edition2018 => Edition::Edition2018, - }; - let cfg_options = { - let mut opts = CfgOptions::default(); - for cfg in &krate.cfg { - match cfg.find('=') { - None => opts.insert_atom(cfg.into()), - Some(pos) => { - let key = &cfg[..pos]; - let value = cfg[pos + 1..].trim_matches('"'); - opts.insert_key_value(key.into(), value.into()); - } - } - } - opts - }; let mut env = Env::default(); if let Some(out_dir) = &krate.out_dir { @@ -290,13 +267,13 @@ impl ProjectWorkspace { .map(|it| proc_macro_client.by_dylib_path(&it)); // FIXME: No crate name in json definition such that we cannot add OUT_DIR to env Some(( - project_json::CrateId(seq_index), + CrateId(seq_index as u32), crate_graph.add_crate_root( file_id, - edition, + krate.edition, // FIXME json definitions can store the crate name None, - cfg_options, + krate.cfg.clone(), env, proc_macro.unwrap_or_default(), ), @@ -306,8 +283,8 @@ impl ProjectWorkspace { for (id, krate) in project.crates.iter().enumerate() { for dep in &krate.deps { - let from_crate_id = project_json::CrateId(id); - let to_crate_id = dep.krate; + let from_crate_id = CrateId(id as u32); + let to_crate_id = dep.crate_id; if let (Some(&from), Some(&to)) = (crates.get(&from_crate_id), crates.get(&to_crate_id)) { @@ -523,7 +500,7 @@ impl ProjectWorkspace { crate_graph } - pub fn workspace_root_for(&self, path: &Path) -> Option<&Path> { + pub fn workspace_root_for(&self, path: &Path) -> Option<&AbsPath> { match self { ProjectWorkspace::Cargo { cargo, .. } => { Some(cargo.workspace_root()).filter(|root| path.starts_with(root)) @@ -531,7 +508,7 @@ impl ProjectWorkspace { ProjectWorkspace::Json { project: ProjectJson { roots, .. }, .. } => roots .iter() .find(|root| path.starts_with(&root.path)) - .map(|root| root.path.as_ref()), + .map(|root| root.path.as_path()), } } } diff --git a/crates/ra_project_model/src/project_json.rs b/crates/ra_project_model/src/project_json.rs index e663bb4d7..4b5dcd634 100644 --- a/crates/ra_project_model/src/project_json.rs +++ b/crates/ra_project_model/src/project_json.rs @@ -2,11 +2,15 @@ use std::path::PathBuf; +use paths::{AbsPath, AbsPathBuf}; +use ra_cfg::CfgOptions; +use ra_db::{CrateId, Dependency, Edition}; use rustc_hash::FxHashSet; use serde::Deserialize; +use stdx::split_delim; /// Roots and crates that compose this Rust project. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug)] pub struct ProjectJson { pub(crate) roots: Vec, pub(crate) crates: Vec, @@ -14,82 +18,100 @@ pub struct ProjectJson { /// A root points to the directory which contains Rust crates. rust-analyzer watches all files in /// all roots. Roots might be nested. -#[derive(Clone, Debug, Deserialize)] -#[serde(transparent)] +#[derive(Clone, Debug)] pub struct Root { - pub(crate) path: PathBuf, + pub(crate) path: AbsPathBuf, } /// A crate points to the root module of a crate and lists the dependencies of the crate. This is /// useful in creating the crate graph. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug)] pub struct Crate { - pub(crate) root_module: PathBuf, + pub(crate) root_module: AbsPathBuf, pub(crate) edition: Edition, - pub(crate) deps: Vec, + pub(crate) deps: Vec, + pub(crate) cfg: CfgOptions, + pub(crate) out_dir: Option, + pub(crate) proc_macro_dylib_path: Option, +} - #[serde(default)] - pub(crate) cfg: FxHashSet, +impl ProjectJson { + pub fn new(base: &AbsPath, data: ProjectJsonData) -> ProjectJson { + ProjectJson { + roots: data.roots.into_iter().map(|path| Root { path: base.join(path) }).collect(), + crates: data + .crates + .into_iter() + .map(|crate_data| Crate { + root_module: base.join(crate_data.root_module), + edition: crate_data.edition.into(), + deps: crate_data + .deps + .into_iter() + .map(|dep_data| Dependency { + crate_id: CrateId(dep_data.krate as u32), + name: dep_data.name.into(), + }) + .collect::>(), + cfg: { + let mut cfg = CfgOptions::default(); + for entry in &crate_data.cfg { + match split_delim(entry, '=') { + Some((key, value)) => { + cfg.insert_key_value(key.into(), value.into()); + } + None => cfg.insert_atom(entry.into()), + } + } + cfg + }, + out_dir: crate_data.out_dir.map(|it| base.join(it)), + proc_macro_dylib_path: crate_data.proc_macro_dylib_path.map(|it| base.join(it)), + }) + .collect::>(), + } + } +} - pub(crate) out_dir: Option, - pub(crate) proc_macro_dylib_path: Option, +#[derive(Deserialize)] +pub struct ProjectJsonData { + roots: Vec, + crates: Vec, } -#[derive(Clone, Copy, Debug, Deserialize)] +#[derive(Deserialize)] +struct CrateData { + root_module: PathBuf, + edition: EditionData, + deps: Vec, + #[serde(default)] + cfg: FxHashSet, + out_dir: Option, + proc_macro_dylib_path: Option, +} + +#[derive(Deserialize)] #[serde(rename = "edition")] -pub enum Edition { +enum EditionData { #[serde(rename = "2015")] Edition2015, #[serde(rename = "2018")] Edition2018, } -/// Identifies a crate by position in the crates array. -#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[serde(transparent)] -pub struct CrateId(pub usize); - -/// A dependency of a crate, identified by its id in the crates array and name. -#[derive(Clone, Debug, Deserialize)] -pub struct Dep { - #[serde(rename = "crate")] - pub(crate) krate: CrateId, - pub(crate) name: String, +impl From for Edition { + fn from(data: EditionData) -> Self { + match data { + EditionData::Edition2015 => Edition::Edition2015, + EditionData::Edition2018 => Edition::Edition2018, + } + } } -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_crate_deserialization() { - let raw_json = json!( { - "crate_id": 2, - "root_module": "this/is/a/file/path.rs", - "deps": [ - { - "crate": 1, - "name": "some_dep_crate" - }, - ], - "edition": "2015", - "cfg": [ - "atom_1", - "atom_2", - "feature=feature_1", - "feature=feature_2", - "other=value", - ], - - }); - - let krate: Crate = serde_json::from_value(raw_json).unwrap(); - - assert!(krate.cfg.contains(&"atom_1".to_string())); - assert!(krate.cfg.contains(&"atom_2".to_string())); - assert!(krate.cfg.contains(&"feature=feature_1".to_string())); - assert!(krate.cfg.contains(&"feature=feature_2".to_string())); - assert!(krate.cfg.contains(&"other=value".to_string())); - } +#[derive(Deserialize)] +struct DepData { + /// Identifies a crate by position in the crates array. + #[serde(rename = "crate")] + krate: usize, + name: String, } diff --git a/crates/rust-analyzer/src/cargo_target_spec.rs b/crates/rust-analyzer/src/cargo_target_spec.rs index 65f90c83c..e98d0f868 100644 --- a/crates/rust-analyzer/src/cargo_target_spec.rs +++ b/crates/rust-analyzer/src/cargo_target_spec.rs @@ -1,8 +1,7 @@ //! See `CargoTargetSpec` -use std::path::PathBuf; - use ra_cfg::CfgExpr; +use ra_db::AbsPathBuf; use ra_ide::{FileId, RunnableKind, TestId}; use ra_project_model::{self, TargetKind}; @@ -14,7 +13,7 @@ use crate::{global_state::GlobalStateSnapshot, Result}; /// build/test/run the target. #[derive(Clone)] pub(crate) struct CargoTargetSpec { - pub(crate) workspace_root: PathBuf, + pub(crate) workspace_root: AbsPathBuf, pub(crate) package: String, pub(crate) target: String, pub(crate) target_kind: TargetKind, diff --git a/crates/rust-analyzer/src/cli/load_cargo.rs b/crates/rust-analyzer/src/cli/load_cargo.rs index e910db6eb..9c510bb62 100644 --- a/crates/rust-analyzer/src/cli/load_cargo.rs +++ b/crates/rust-analyzer/src/cli/load_cargo.rs @@ -1,13 +1,13 @@ //! Loads a Cargo project into a static instance of analysis, without support //! for incorporating changes. -use std::{convert::TryFrom, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; use anyhow::Result; use crossbeam_channel::{unbounded, Receiver}; use ra_db::{AbsPathBuf, CrateGraph}; use ra_ide::{AnalysisChange, AnalysisHost}; use ra_project_model::{CargoConfig, ProcMacroClient, ProjectManifest, ProjectWorkspace}; -use vfs::loader::Handle; +use vfs::{loader::Handle, AbsPath}; use crate::global_state::{ProjectFolders, SourceRootConfig}; @@ -39,10 +39,9 @@ pub fn load_cargo( ProcMacroClient::dummy() }; - let crate_graph = ws.to_crate_graph(None, &proc_macro_client, &mut |path: &Path| { - let path = AbsPathBuf::try_from(path.to_path_buf()).unwrap(); - let contents = loader.load_sync(&path); - let path = vfs::VfsPath::from(path); + let crate_graph = ws.to_crate_graph(None, &proc_macro_client, &mut |path: &AbsPath| { + let contents = loader.load_sync(path); + let path = vfs::VfsPath::from(path.to_path_buf()); vfs.set_file_contents(path.clone(), contents); vfs.file_id(&path) }); diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 7eded04c5..8f69de968 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -14,7 +14,7 @@ use lsp_types::ClientCapabilities; use ra_db::AbsPathBuf; use ra_flycheck::FlycheckConfig; use ra_ide::{AssistConfig, CompletionConfig, HoverConfig, InlayHintsConfig}; -use ra_project_model::{CargoConfig, ProjectJson, ProjectManifest}; +use ra_project_model::{CargoConfig, ProjectJson, ProjectJsonData, ProjectManifest}; use serde::Deserialize; #[derive(Debug, Clone)] @@ -273,19 +273,19 @@ impl Config { self.lens = LensConfig::NO_LENS; } - if let Some(linked_projects) = get::>(value, "/linkedProjects") { + if let Some(linked_projects) = get::>(value, "/linkedProjects") { if !linked_projects.is_empty() { self.linked_projects.clear(); for linked_project in linked_projects { let linked_project = match linked_project { - ManifestOrJsonProject::Manifest(it) => { + ManifestOrProjectJson::Manifest(it) => { let path = self.root_path.join(it); match ProjectManifest::from_manifest_file(path) { Ok(it) => it.into(), Err(_) => continue, } } - ManifestOrJsonProject::JsonProject(it) => it.into(), + ManifestOrProjectJson::ProjectJson(it) => ProjectJson::new(&self.root_path, it).into(), }; self.linked_projects.push(linked_project); } @@ -371,7 +371,7 @@ impl Config { #[derive(Deserialize)] #[serde(untagged)] -enum ManifestOrJsonProject { +enum ManifestOrProjectJson { Manifest(PathBuf), - JsonProject(ProjectJson), + ProjectJson(ProjectJsonData), } diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index f18694feb..e7eeb60ee 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -3,7 +3,7 @@ //! //! Each tick provides an immutable snapshot of the state as `WorldSnapshot`. -use std::{convert::TryFrom, path::Path, sync::Arc}; +use std::{convert::TryFrom, sync::Arc}; use crossbeam_channel::{unbounded, Receiver}; use lsp_types::Url; @@ -13,7 +13,7 @@ use ra_flycheck::{Flycheck, FlycheckConfig}; use ra_ide::{Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId}; use ra_project_model::{CargoWorkspace, ProcMacroClient, ProjectWorkspace, Target}; use stdx::format_to; -use vfs::{file_set::FileSetConfig, loader::Handle, AbsPathBuf}; +use vfs::{file_set::FileSetConfig, loader::Handle, AbsPath, AbsPathBuf}; use crate::{ config::{Config, FilesWatcher}, @@ -31,7 +31,7 @@ fn create_flycheck(workspaces: &[ProjectWorkspace], config: &FlycheckConfig) -> workspaces.iter().find_map(|w| match w { ProjectWorkspace::Cargo { cargo, .. } => { let cargo_project_root = cargo.workspace_root().to_path_buf(); - Some(Flycheck::new(config.clone(), cargo_project_root)) + Some(Flycheck::new(config.clone(), cargo_project_root.into())) } ProjectWorkspace::Json { .. } => { log::warn!("Cargo check watching only supported for cargo workspaces, disabling"); @@ -112,10 +112,9 @@ impl GlobalState { // Create crate graph from all the workspaces let mut crate_graph = CrateGraph::default(); - let mut load = |path: &Path| { - let path = AbsPathBuf::try_from(path.to_path_buf()).ok()?; - let contents = loader.load_sync(&path); - let path = vfs::VfsPath::from(path); + let mut load = |path: &AbsPath| { + let contents = loader.load_sync(path); + let path = vfs::VfsPath::from(path.to_path_buf()); vfs.set_file_contents(path.clone(), contents); vfs.file_id(&path) }; diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index b9d296856..390c66dfc 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -114,10 +114,7 @@ pub fn main_loop(config: Config, connection: Connection) -> Result<()> { .ok() } LinkedProject::InlineJsonProject(it) => { - Some(ra_project_model::ProjectWorkspace::Json { - project: it.clone(), - project_location: config.root_path.clone(), - }) + Some(ra_project_model::ProjectWorkspace::Json { project: it.clone() }) } }) .collect::>() diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index 221b902b2..a1e2432cf 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -419,7 +419,7 @@ pub fn handle_runnables( location: None, kind: lsp_ext::RunnableKind::Cargo, args: lsp_ext::CargoRunnable { - workspace_root: Some(spec.workspace_root.clone()), + workspace_root: Some(spec.workspace_root.clone().into()), cargo_args: vec![ cmd.to_string(), "--package".to_string(), diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index da9887a9a..88d1c0d8a 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -663,7 +663,7 @@ pub(crate) fn runnable( location: Some(location), kind: lsp_ext::RunnableKind::Cargo, args: lsp_ext::CargoRunnable { - workspace_root: workspace_root, + workspace_root: workspace_root.map(|it| it.into()), cargo_args, executable_args, }, diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs index baee6ddc8..5b4978285 100644 --- a/crates/vfs-notify/src/lib.rs +++ b/crates/vfs-notify/src/lib.rs @@ -45,7 +45,7 @@ impl loader::Handle for LoaderHandle { fn invalidate(&mut self, path: AbsPathBuf) { self.sender.send(Message::Invalidate(path)).unwrap(); } - fn load_sync(&mut self, path: &AbsPathBuf) -> Option> { + fn load_sync(&mut self, path: &AbsPath) -> Option> { read(path) } } diff --git a/crates/vfs/src/loader.rs b/crates/vfs/src/loader.rs index a216b5f13..052803dd9 100644 --- a/crates/vfs/src/loader.rs +++ b/crates/vfs/src/loader.rs @@ -1,7 +1,7 @@ //! Object safe interface for file watching and reading. use std::fmt; -use paths::AbsPathBuf; +use paths::{AbsPath, AbsPathBuf}; #[derive(Debug)] pub enum Entry { @@ -28,7 +28,7 @@ pub trait Handle: fmt::Debug { Self: Sized; fn set_config(&mut self, config: Config); fn invalidate(&mut self, path: AbsPathBuf); - fn load_sync(&mut self, path: &AbsPathBuf) -> Option>; + fn load_sync(&mut self, path: &AbsPath) -> Option>; } impl Entry { diff --git a/docs/user/manual.adoc b/docs/user/manual.adoc index f1b7ed7fc..0dc1dc2ae 100644 --- a/docs/user/manual.adoc +++ b/docs/user/manual.adoc @@ -322,6 +322,8 @@ There are tree ways to feed `rust-project.json` to rust-analyzer: * Specify `"rust-analyzer.linkedProjects": [ "path/to/rust-project.json" ]` in the settings (and make sure that your LSP client sends settings as a part of initialize request). * Specify `"rust-analyzer.linkedProjects": [ { "roots": [...], "crates": [...] }]` inline. +Relative paths are interpreted relative to `rust-project.json` file location or (for inline JSON) relative to `rootUri`. + See https://github.com/rust-analyzer/rust-project.json-example for a small example. == Features -- cgit v1.2.3