Repository: rust-lang/rust-analyzer
Pull request #17118 (2024-04-21 12:41:40)

feat: Allow rust files to be used linkedProjects
With this, script files become more usable as the user can at least add them manually to the linked projects, allowing them to be used "on the (manual) fly" without having to open a separate vscode window that only has files open and no folder.

Also makes build scripts work for them (though no proc-macros, for some reason the dylib field is not populated in the output)

Veykril: @bors r+
bors: :pushpin: Commit af3fc73727dd3a7e20bf6b6c2b9afb49fae59702 has been approved by `Veykril`

It is now in the [queue](https://bors.rust-lang.org/homu/queue/rust-analyzer) for this repository.

<!-- @bors r=Veykril af3fc73727dd3a7e20bf6b6c2b9afb49fae59702 -->
<!-- homu: {"type":"Approved","sha":"af3fc73727dd3a7e20bf6b6c2b9afb49fae59702","approver":"Veykril","queue":"https://bors.rust-lang.org/homu/queue/rust-analyzer"} -->
bors: :broken_heart: Test failed - [checks-actions](https://github.com/rust-lang/rust-analyzer/actions/runs/8772765374/job/24072738066)
<!-- homu: {"type":"BuildFailed","builder_url":"https://github.com/rust-lang/rust-analyzer/actions/runs/8772765374/job/24072738066","builder_name":"checks-actions"} -->

Patch:
diff --git a/crates/base-db/src/lib.rs b/crates/base-db/src/lib.rs
index 2b64a07a5a..82aff6e3b8 100644
--- a/crates/base-db/src/lib.rs
+++ b/crates/base-db/src/lib.rs
@@ -51,6 +51,7 @@ pub trait FileLoader {
     /// Text of the file.
     fn file_text(&self, file_id: FileId) -> Arc<str>;
     fn resolve_path(&self, path: AnchoredPath<'_>) -> Option<FileId>;
+    /// Crates whose root's source root is the same as the source root of `file_id`
     fn relevant_crates(&self, file_id: FileId) -> Arc<[CrateId]>;
 }
 
@@ -104,6 +105,7 @@ pub trait SourceDatabaseExt: SourceDatabase {
     #[salsa::input]
     fn source_root(&self, id: SourceRootId) -> Arc<SourceRoot>;
 
+    /// Crates whose root fool is in `id`.
     fn source_root_crates(&self, id: SourceRootId) -> Arc<[CrateId]>;
 }
 
diff --git a/crates/hir/src/semantics/source_to_def.rs b/crates/hir/src/semantics/source_to_def.rs
index 434e4b5a0c..007709a836 100644
--- a/crates/hir/src/semantics/source_to_def.rs
+++ b/crates/hir/src/semantics/source_to_def.rs
@@ -121,7 +121,7 @@ impl SourceToDefCtx<'_, '_> {
         let _p = tracing::span!(tracing::Level::INFO, "SourceBinder::file_to_module_def").entered();
         let mut mods = SmallVec::new();
         for &crate_id in self.db.relevant_crates(file).iter() {
-            // FIXME: inner items
+            // Note: `mod` declarations in block modules cannot be supported here
             let crate_def_map = self.db.crate_def_map(crate_id);
             mods.extend(
                 crate_def_map
@@ -129,6 +129,9 @@ impl SourceToDefCtx<'_, '_> {
                     .map(|local_id| crate_def_map.module_id(local_id)),
             )
         }
+        if mods.is_empty() {
+            // FIXME: detached file
+        }
         mods
     }
 
diff --git a/crates/ide-db/src/prime_caches.rs b/crates/ide-db/src/prime_caches.rs
index 024e8f6ae3..0db87c6bc4 100644
--- a/crates/ide-db/src/prime_caches.rs
+++ b/crates/ide-db/src/prime_caches.rs
@@ -1,6 +1,6 @@
 //! rust-analyzer is lazy and doesn't compute anything unless asked. This
 //! sometimes is counter productive when, for example, the first goto definition
-//! request takes longer to compute. This modules implemented prepopulation of
+//! request takes longer to compute. This module implements prepopulation of
 //! various caches, it's not really advanced at the moment.
 mod topologic_sort;
 
diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs
index fbd423c9ea..3fdd59967b 100644
--- a/crates/project-model/src/build_scripts.rs
+++ b/crates/project-model/src/build_scripts.rs
@@ -24,7 +24,7 @@ use toolchain::Tool;
 
 use crate::{
     cfg::CfgFlag, utf8_stdout, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation,
-    InvocationStrategy, Package, Sysroot, TargetKind,
+    InvocationStrategy, ManifestPath, Package, Sysroot, TargetKind,
 };
 
 /// Output of the build script and proc-macro building steps for a workspace.
@@ -63,7 +63,7 @@ impl WorkspaceBuildScripts {
     fn build_command(
         config: &CargoConfig,
         allowed_features: &FxHashSet<String>,
-        workspace_root: &AbsPathBuf,
+        manifest_path: &ManifestPath,
         sysroot: Option<&Sysroot>,
     ) -> io::Result<Command> {
         let mut cmd = match config.run_build_script_command.as_deref() {
@@ -79,7 +79,7 @@ impl WorkspaceBuildScripts {
                 cmd.args(&config.extra_args);
 
                 cmd.arg("--manifest-path");
-                cmd.arg(workspace_root.join("Cargo.toml"));
+                cmd.arg(manifest_path.as_ref());
 
                 if let Some(target_dir) = &config.target_dir {
                     cmd.arg("--target-dir").arg(target_dir);
@@ -116,6 +116,10 @@ impl WorkspaceBuildScripts {
                     }
                 }
 
+                if manifest_path.extension().map_or(false, |ext| ext == "rs") {
+                    cmd.arg("-Zscript");
+                }
+
                 cmd
             }
         };
@@ -152,37 +156,12 @@ impl WorkspaceBuildScripts {
         .as_ref();
 
         let allowed_features = workspace.workspace_features();
-
-        match Self::run_per_ws(
-            Self::build_command(
-                config,
-                &allowed_features,
-                &workspace.workspace_root().to_path_buf(),
-                sysroot,
-            )?,
-            workspace,
-            current_dir,
-            progress,
-        ) {
-            Ok(WorkspaceBuildScripts { error: Some(error), .. })
-                if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_75) =>
-            {
-                // building build scripts failed, attempt to build with --keep-going so
-                // that we potentially get more build data
-                let mut cmd = Self::build_command(
-                    config,
-                    &allowed_features,
-                    &workspace.workspace_root().to_path_buf(),
-                    sysroot,
-                )?;
-
-                cmd.args(["--keep-going"]);
-                let mut res = Self::run_per_ws(cmd, workspace, current_dir, progress)?;
-                res.error = Some(error);
-                Ok(res)
-            }
-            res => res,
+        let mut cmd =
+            Self::build_command(config, &allowed_features, workspace.manifest_path(), sysroot)?;
+        if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_75) {
+            cmd.args(["--keep-going"]);
         }
+        Self::run_per_ws(cmd, workspace, current_dir, progress)
     }
 
     /// Runs the build scripts by invoking the configured command *once*.
@@ -204,7 +183,13 @@ impl WorkspaceBuildScripts {
                 ))
             }
         };
-        let cmd = Self::build_command(config, &Default::default(), workspace_root, None)?;
+        let cmd = Self::build_command(
+            config,
+            &Default::default(),
+            // This is not gonna be used anyways, so just construct a dummy here
+            &ManifestPath::try_from(workspace_root.clone()).unwrap(),
+            None,
+        )?;
         // NB: Cargo.toml could have been modified between `cargo metadata` and
         // `cargo check`. We shouldn't assume that package ids we see here are
         // exactly those from `config`.
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index ff7cf144aa..88ba5a0da9 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -32,6 +32,7 @@ pub struct CargoWorkspace {
     targets: Arena<TargetData>,
     workspace_root: AbsPathBuf,
     target_directory: AbsPathBuf,
+    manifest_path: ManifestPath,
 }
 
 impl ops::Index<Package> for CargoWorkspace {
@@ -334,7 +335,7 @@ impl CargoWorkspace {
         .with_context(|| format!("Failed to run `{:?}`", meta.cargo_command()))
     }
 
-    pub fn new(mut meta: cargo_metadata::Metadata) -> CargoWorkspace {
+    pub fn new(mut meta: cargo_metadata::Metadata, manifest_path: ManifestPath) -> CargoWorkspace {
         let mut pkg_by_id = FxHashMap::default();
         let mut packages = Arena::default();
         let mut targets = Arena::default();
@@ -448,7 +449,7 @@ impl CargoWorkspace {
 
         let target_directory = AbsPathBuf::assert(meta.target_directory);
 
-        CargoWorkspace { packages, targets, workspace_root, target_directory }
+        CargoWorkspace { packages, targets, workspace_root, target_directory, manifest_path }
     }
 
     pub fn packages(&self) -> impl ExactSizeIterator<Item = Package> + '_ {
@@ -466,6 +467,10 @@ impl CargoWorkspace {
         &self.workspace_root
     }
 
+    pub fn manifest_path(&self) -> &ManifestPath {
+        &self.manifest_path
+    }
+
     pub fn target_directory(&self) -> &AbsPath {
         &self.target_directory
     }
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index 7f3e35ca5d..5428f061b7 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -54,11 +54,13 @@ pub use crate::{
     sysroot::Sysroot,
     workspace::{FileLoader, PackageRoot, ProjectWorkspace},
 };
+pub use cargo_metadata::Metadata;
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
 pub enum ProjectManifest {
     ProjectJson(ManifestPath),
     CargoToml(ManifestPath),
+    CargoScript(ManifestPath),
 }
 
 impl ProjectManifest {
@@ -71,7 +73,10 @@ impl ProjectManifest {
         if path.file_name().unwrap_or_default() == "Cargo.toml" {
             return Ok(ProjectManifest::CargoToml(path));
         }
-        bail!("project root must point to Cargo.toml or rust-project.json: {path}");
+        if path.extension().unwrap_or_default() == "rs" {
+            return Ok(ProjectManifest::CargoScript(path));
+        }
+        bail!("project root must point to a Cargo.toml, rust-project.json or <script>.rs file: {path}");
     }
 
     pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> {
@@ -146,15 +151,19 @@ impl ProjectManifest {
         res.sort();
         res
     }
+
+    pub fn manifest_path(&self) -> &ManifestPath {
+        match self {
+            ProjectManifest::ProjectJson(it)
+            | ProjectManifest::CargoToml(it)
+            | ProjectManifest::CargoScript(it) => it,
+        }
+    }
 }
 
 impl fmt::Display for ProjectManifest {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            ProjectManifest::ProjectJson(it) | ProjectManifest::CargoToml(it) => {
-                fmt::Display::fmt(&it, f)
-            }
-        }
+        fmt::Display::fmt(self.manifest_path(), f)
     }
 }
 
diff --git a/crates/project-model/src/manifest_path.rs b/crates/project-model/src/manifest_path.rs
index d86e81e7e1..6929a75895 100644
--- a/crates/project-model/src/manifest_path.rs
+++ b/crates/project-model/src/manifest_path.rs
@@ -1,5 +1,5 @@
 //! See [`ManifestPath`].
-use std::{fmt, ops, path::Path};
+use std::{borrow::Borrow, fmt, ops};
 
 use paths::{AbsPath, AbsPathBuf};
 
@@ -54,8 +54,14 @@ impl ops::Deref for ManifestPath {
     }
 }
 
-impl AsRef<Path> for ManifestPath {
-    fn as_ref(&self) -> &Path {
+impl AsRef<AbsPath> for ManifestPath {
+    fn as_ref(&self) -> &AbsPath {
         self.file.as_ref()
     }
 }
+
+impl Borrow<AbsPath> for ManifestPath {
+    fn borrow(&self) -> &AbsPath {
+        self.file.borrow()
+    }
+}
diff --git a/crates/project-model/src/sysroot.rs b/crates/project-model/src/sysroot.rs
index 1142d6243d..00dd18b5b1 100644
--- a/crates/project-model/src/sysroot.rs
+++ b/crates/project-model/src/sysroot.rs
@@ -349,7 +349,7 @@ impl Sysroot {
                     .filter(|&package| RELEVANT_SYSROOT_CRATES.contains(&&*package.name))
                     .map(|package| package.id.clone())
                     .collect();
-                let cargo_workspace = CargoWorkspace::new(res);
+                let cargo_workspace = CargoWorkspace::new(res, sysroot_cargo_toml);
                 Some(Sysroot {
                     root: sysroot_dir.clone(),
                     src_root: Some(Ok(sysroot_src_dir.clone())),
@@ -368,7 +368,7 @@ impl Sysroot {
                 .into_iter()
                 .map(|it| sysroot_src_dir.join(it))
                 .filter_map(|it| ManifestPath::try_from(it).ok())
-                .find(|it| fs::metadata(it).is_ok());
+                .find(|it| fs::metadata(it.as_ref()).is_ok());
 
             if let Some(root) = root {
                 stitched.crates.alloc(SysrootCrateData {
@@ -468,7 +468,7 @@ fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
     let rustc_src = sysroot_path.join("lib/rustlib/rustc-src/rust/compiler/rustc/Cargo.toml");
     let rustc_src = ManifestPath::try_from(rustc_src).ok()?;
     tracing::debug!("checking for rustc source code: {rustc_src}");
-    if fs::metadata(&rustc_src).is_ok() {
+    if fs::metadata(rustc_src.as_ref()).is_ok() {
         Some(rustc_src)
     } else {
         None
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 98c5a02dcd..9f6e75171b 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -4,7 +4,7 @@
 
 use std::{collections::VecDeque, fmt, fs, iter, sync};
 
-use anyhow::{format_err, Context};
+use anyhow::Context;
 use base_db::{
     CrateDisplayName, CrateGraph, CrateId, CrateName, CrateOrigin, Dependency, Env, FileId,
     LangCrateOrigin, ProcMacroPaths, TargetLayoutLoadResult,
@@ -14,7 +14,6 @@ use paths::{AbsPath, AbsPathBuf};
 use rustc_hash::{FxHashMap, FxHashSet};
 use semver::Version;
 use span::Edition;
-use stdx::always;
 use toolchain::Tool;
 use triomphe::Arc;
 
@@ -101,7 +100,7 @@ pub enum ProjectWorkspace {
     /// Backed by basic sysroot crates for basic completion and highlighting.
     DetachedFile {
         /// The file in question.
-        file: AbsPathBuf,
+        file: ManifestPath,
         /// The sysroot loaded for this workspace.
         sysroot: Result<Sysroot, Option<String>>,
         /// Holds cfg flags for the current target. We get those by running
@@ -116,7 +115,7 @@ pub enum ProjectWorkspace {
         /// A set of cfg overrides for the files.
         cfg_overrides: CfgOverrides,
         /// Is this file a cargo script file?
-        cargo_script: Option<CargoWorkspace>,
+        cargo_script: Option<(CargoWorkspace, WorkspaceBuildScripts)>,
     },
 }
 
@@ -230,7 +229,7 @@ impl ProjectWorkspace {
     ) -> anyhow::Result<ProjectWorkspace> {
         let res = match manifest {
             ProjectManifest::ProjectJson(project_json) => {
-                let file = fs::read_to_string(project_json)
+                let file = fs::read_to_string(project_json.as_ref())
                     .with_context(|| format!("Failed to read json file {project_json}"))?;
                 let data = serde_json::from_str(&file)
                     .with_context(|| format!("Failed to deserialize json file {project_json}"))?;
@@ -243,6 +242,9 @@ impl ProjectWorkspace {
                     &config.cfg_overrides,
                 )
             }
+            ProjectManifest::CargoScript(rust_file) => {
+                ProjectWorkspace::load_detached_file(rust_file, config)?
+            }
             ProjectManifest::CargoToml(cargo_toml) => {
                 let sysroot = match (&config.sysroot, &config.sysroot_src) {
                     (Some(RustLibSource::Path(path)), None) => {
@@ -299,7 +301,7 @@ impl ProjectWorkspace {
                         progress,
                     ) {
                         Ok(meta) => {
-                            let workspace = CargoWorkspace::new(meta);
+                            let workspace = CargoWorkspace::new(meta, cargo_toml.clone());
                             let buildscripts = WorkspaceBuildScripts::rustc_crates(
                                 &workspace,
                                 cargo_toml.parent(),
@@ -355,7 +357,7 @@ impl ProjectWorkspace {
                         "Failed to read Cargo metadata from Cargo.toml file {cargo_toml}, {toolchain:?}",
                     )
                 })?;
-                let cargo = CargoWorkspace::new(meta);
+                let cargo = CargoWorkspace::new(meta, cargo_toml.clone());
 
                 let cargo_config_extra_env =
                     cargo_config_env(cargo_toml, &config.extra_env, sysroot_ref);
@@ -433,82 +435,71 @@ impl ProjectWorkspace {
         }
     }
 
-    pub fn load_detached_files(
-        detached_files: Vec<AbsPathBuf>,
+    pub fn load_detached_file(
+        detached_file: &ManifestPath,
         config: &CargoConfig,
-    ) -> Vec<anyhow::Result<ProjectWorkspace>> {
-        detached_files
-            .into_iter()
-            .map(|detached_file| {
-                let dir = detached_file
-                    .parent()
-                    .ok_or_else(|| format_err!("detached file has no parent"))?;
-                let sysroot = match &config.sysroot {
-                    Some(RustLibSource::Path(path)) => {
-                        Sysroot::with_sysroot_dir(path.clone(), config.sysroot_query_metadata)
-                            .map_err(|e| Some(format!("Failed to find sysroot at {path}:{e}")))
-                    }
-                    Some(RustLibSource::Discover) => {
-                        Sysroot::discover(dir, &config.extra_env, config.sysroot_query_metadata)
-                            .map_err(|e| {
-                                Some(format!(
-                                    "Failed to find sysroot for {dir}. Is rust-src installed? {e}"
-                                ))
-                            })
-                    }
-                    None => Err(None),
-                };
+    ) -> anyhow::Result<ProjectWorkspace> {
+        let dir = detached_file.parent();
+        let sysroot = match &config.sysroot {
+            Some(RustLibSource::Path(path)) => {
+                Sysroot::with_sysroot_dir(path.clone(), config.sysroot_query_metadata)
+                    .map_err(|e| Some(format!("Failed to find sysroot at {path}:{e}")))
+            }
+            Some(RustLibSource::Discover) => Sysroot::discover(
+                dir,
+                &config.extra_env,
+                config.sysroot_query_metadata,
+            )
+            .map_err(|e| {
+                Some(format!("Failed to find sysroot for {dir}. Is rust-src installed? {e}"))
+            }),
+            None => Err(None),
+        };
 
-                let sysroot_ref = sysroot.as_ref().ok();
-                let toolchain = match get_toolchain_version(
-                    dir,
-                    sysroot_ref,
-                    Tool::Rustc,
-                    &config.extra_env,
-                    "rustc ",
-                ) {
-                    Ok(it) => it,
-                    Err(e) => {
-                        tracing::error!("{e}");
-                        None
-                    }
-                };
+        let sysroot_ref = sysroot.as_ref().ok();
+        let toolchain =
+            match get_toolchain_version(dir, sysroot_ref, Tool::Rustc, &config.extra_env, "rustc ")
+            {
+                Ok(it) => it,
+                Err(e) => {
+                    tracing::error!("{e}");
+                    None
+                }
+            };
 
-                let rustc_cfg =
-                    rustc_cfg::get(None, &config.extra_env, RustcCfgConfig::Rustc(sysroot_ref));
-                let data_layout = target_data_layout::get(
-                    RustcDataLayoutConfig::Rustc(sysroot_ref),
-                    None,
-                    &config.extra_env,
-                );
+        let rustc_cfg = rustc_cfg::get(None, &config.extra_env, RustcCfgConfig::Rustc(sysroot_ref));
+        let data_layout = target_data_layout::get(
+            RustcDataLayoutConfig::Rustc(sysroot_ref),
+            None,
+            &config.extra_env,
+        );
 
-                let cargo_script = ManifestPath::try_from(detached_file.clone())
-                    .ok()
-                    .and_then(|file| {
-                        CargoWorkspace::fetch_metadata(
-                            &file,
-                            file.parent(),
-                            config,
-                            sysroot_ref,
-                            &|_| (),
-                        )
-                        .ok()
-                    })
-                    .map(CargoWorkspace::new);
+        let cargo_script =
+            CargoWorkspace::fetch_metadata(detached_file, dir, config, sysroot_ref, &|_| ())
+                .ok()
+                .map(|ws| {
+                    (
+                        CargoWorkspace::new(ws, detached_file.clone()),
+                        WorkspaceBuildScripts::default(),
+                    )
+                });
 
-                Ok(ProjectWorkspace::DetachedFile {
-                    file: detached_file,
-                    sysroot,
-                    rustc_cfg,
-                    toolchain,
-                    target_layout: data_layout
-                        .map(Arc::from)
-                        .map_err(|it| Arc::from(it.to_string())),
-                    cfg_overrides: config.cfg_overrides.clone(),
-                    cargo_script,
-                })
-            })
-            .collect()
+        Ok(ProjectWorkspace::DetachedFile {
+            file: detached_file.to_owned(),
+            sysroot,
+            rustc_cfg,
+            toolchain,
+            target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
+            cfg_overrides: config.cfg_overrides.clone(),
+            cargo_script,
+        })
+    }
+
+    pub fn load_detached_files(
+        detached_files: Vec<ManifestPath>,
+        config: &CargoConfig,
+    ) -> Vec<anyhow::Result<ProjectWorkspace>> {
+        detached_files.into_iter().map(|file| Self::load_detached_file(&file, config)).collect()
     }
 
     /// Runs the build scripts for this [`ProjectWorkspace`].
@@ -519,7 +510,7 @@ impl ProjectWorkspace {
     ) -> anyhow::Result<WorkspaceBuildScripts> {
         match self {
             ProjectWorkspace::DetachedFile {
-                cargo_script: Some(cargo),
+                cargo_script: Some((cargo, _)),
                 toolchain,
                 sysroot,
                 ..
@@ -586,10 +577,11 @@ impl ProjectWorkspace {
 
     pub fn set_build_scripts(&mut self, bs: WorkspaceBuildScripts) {
         match self {
-            ProjectWorkspace::Cargo { build_scripts, .. } => *build_scripts = bs,
-            _ => {
-                always!(bs == WorkspaceBuildScripts::default());
+            ProjectWorkspace::Cargo { build_scripts, .. }
+            | ProjectWorkspace::DetachedFile { cargo_script: Some((_, build_scripts)), .. } => {
+                *build_scripts = bs
             }
+            _ => assert_eq!(bs, WorkspaceBuildScripts::default()),
         }
     }
 
@@ -742,15 +734,18 @@ impl ProjectWorkspace {
             ProjectWorkspace::DetachedFile { file, cargo_script, sysroot, .. } => {
                 iter::once(PackageRoot {
                     is_local: true,
-                    include: vec![file.clone()],
+                    include: vec![file.as_ref().to_owned()],
                     exclude: Vec::new(),
                 })
-                .chain(cargo_script.iter().flat_map(|cargo| {
+                .chain(cargo_script.iter().flat_map(|(cargo, build_scripts)| {
                     cargo.packages().map(|pkg| {
                         let is_local = cargo[pkg].is_local;
                         let pkg_root = cargo[pkg].manifest.parent().to_path_buf();
 
                         let mut include = vec![pkg_root.clone()];
+                        let out_dir =
+                            build_scripts.get_output(pkg).and_then(|it| it.out_dir.clone());
+                        include.extend(out_dir);
 
                         // In case target's path is manually set in Cargo.toml to be
                         // outside the package root, add its parent as an extra include.
@@ -801,7 +796,7 @@ impl ProjectWorkspace {
             ProjectWorkspace::DetachedFile { sysroot, cargo_script, .. } => {
                 let sysroot_package_len = sysroot.as_ref().map_or(0, |it| it.num_packages());
                 sysroot_package_len
-                    + cargo_script.as_ref().map_or(1, |cargo| cargo.packages().len())
+                    + cargo_script.as_ref().map_or(1, |(cargo, _)| cargo.packages().len())
             }
         }
     }
@@ -863,7 +858,7 @@ impl ProjectWorkspace {
                 cfg_overrides,
                 cargo_script,
             } => (
-                if let Some(cargo) = cargo_script {
+                if let Some((cargo, build_scripts)) = cargo_script {
                     cargo_to_crate_graph(
                         &mut |path| load(path),
                         None,
@@ -871,7 +866,7 @@ impl ProjectWorkspace {
                         sysroot.as_ref().ok(),
                         rustc_cfg.clone(),
                         cfg_overrides,
-                        &WorkspaceBuildScripts::default(),
+                        build_scripts,
                     )
                 } else {
                     detached_file_to_crate_graph(
@@ -959,7 +954,7 @@ impl ProjectWorkspace {
                     file,
                     sysroot,
                     rustc_cfg,
-                    cargo_script,
+                    cargo_script: Some((cargo_script, _)),
                     toolchain,
                     target_layout,
                     cfg_overrides,
@@ -968,7 +963,7 @@ impl ProjectWorkspace {
                     file: o_file,
                     sysroot: o_sysroot,
                     rustc_cfg: o_rustc_cfg,
-                    cargo_script: o_cargo_script,
+                    cargo_script: Some((o_cargo_script, _)),
                     toolchain: o_toolchain,
                     target_layout: o_target_layout,
                     cfg_overrides: o_cfg_overrides,
@@ -1294,11 +1289,11 @@ fn cargo_to_crate_graph(
 fn detached_file_to_crate_graph(
     rustc_cfg: Vec<CfgFlag>,
     load: FileLoader<'_>,
-    detached_file: &AbsPathBuf,
+    detached_file: &ManifestPath,
     sysroot: Option<&Sysroot>,
     override_cfg: &CfgOverrides,
 ) -> (CrateGraph, ProcMacroPaths) {
-    let _p = tracing::span!(tracing::Level::INFO, "detached_files_to_crate_graph").entered();
+    let _p = tracing::span!(tracing::Level::INFO, "detached_file_to_crate_graph").entered();
     let mut crate_graph = CrateGraph::default();
     let (public_deps, _libproc_macro) = match sysroot {
         Some(sysroot) => sysroot_to_crate_graph(&mut crate_graph, sysroot, rustc_cfg.clone(), load),
diff --git a/crates/rust-analyzer/src/cli/progress_report.rs b/crates/rust-analyzer/src/cli/progress_report.rs
index 6964977840..8b143daf2a 100644
--- a/crates/rust-analyzer/src/cli/progress_report.rs
+++ b/crates/rust-analyzer/src/cli/progress_report.rs
@@ -96,7 +96,7 @@ impl<'a> ProgressReport<'a> {
     }
 
     fn set_value(&mut self, value: f32) {
-        self.curr = f32::max(0.0, f32::min(1.0, value));
+        self.curr = value.clamp(0.0, 1.0);
     }
 
     fn clear(&mut self) {
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index e956791d9d..5842c1d2ec 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -356,7 +356,8 @@ config_data! {
         /// of projects.
         ///
         /// Elements must be paths pointing to `Cargo.toml`,
-        /// `rust-project.json`, or JSON objects in `rust-project.json` format.
+        /// `rust-project.json`, `.rs` files (which will be treated as standalone files) or JSON
+        /// objects in `rust-project.json` format.
         linkedProjects: Vec<ManifestOrProjectJson> = vec![],
 
         /// Number of syntax trees rust-analyzer keeps in memory. Defaults to 128.
@@ -1301,12 +1302,9 @@ impl Config {
                     self.files_excludeDirs().iter().map(|p| self.root_path.join(p)).collect();
                 self.discovered_projects
                     .iter()
-                    .filter(
-                        |(ProjectManifest::ProjectJson(path)
-                         | ProjectManifest::CargoToml(path))| {
-                            !exclude_dirs.iter().any(|p| path.starts_with(p))
-                        },
-                    )
+                    .filter(|project| {
+                        !exclude_dirs.iter().any(|p| project.manifest_path().starts_with(p))
+                    })
                     .cloned()
                     .map(LinkedProject::from)
                     .collect()
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index e9bca19af6..eaf3f511c4 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -18,7 +18,9 @@ use parking_lot::{
     RwLockWriteGuard,
 };
 use proc_macro_api::ProcMacroServer;
-use project_model::{CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts};
+use project_model::{
+    CargoWorkspace, ManifestPath, ProjectWorkspace, Target, WorkspaceBuildScripts,
+};
 use rustc_hash::{FxHashMap, FxHashSet};
 use triomphe::Arc;
 use vfs::{AnchoredPathBuf, ChangedFile, Vfs};
@@ -125,7 +127,7 @@ pub(crate) struct GlobalState {
     /// to invalidate any salsa caches.
     pub(crate) workspaces: Arc<Vec<ProjectWorkspace>>,
     pub(crate) crate_graph_file_dependencies: FxHashSet<vfs::VfsPath>,
-    pub(crate) detached_files: FxHashSet<vfs::AbsPathBuf>,
+    pub(crate) detached_files: FxHashSet<ManifestPath>,
 
     // op queues
     pub(crate) fetch_workspaces_queue:
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index cf97d7d9d2..39cdef08f8 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -1759,11 +1759,12 @@ pub(crate) fn handle_open_docs(
     let position = from_proto::file_position(&snap, params)?;
 
     let ws_and_sysroot = snap.workspaces.iter().find_map(|ws| match ws {
-        ProjectWorkspace::Cargo { cargo, sysroot, .. } => Some((cargo, sysroot.as_ref().ok())),
-        ProjectWorkspace::Json { .. } => None,
-        ProjectWorkspace::DetachedFile { cargo_script, sysroot, .. } => {
-            cargo_script.as_ref().zip(Some(sysroot.as_ref().ok()))
+        ProjectWorkspace::Cargo { cargo, sysroot, .. }
+        | ProjectWorkspace::DetachedFile { cargo_script: Some((cargo, _)), sysroot, .. } => {
+            Some((cargo, sysroot.as_ref().ok()))
         }
+        ProjectWorkspace::Json { .. } => None,
+        ProjectWorkspace::DetachedFile { .. } => None,
     });
 
     let (cargo, sysroot) = match ws_and_sysroot {
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index 5d8a66cabc..4cc174731c 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -25,7 +25,7 @@ use ide_db::{
 use itertools::Itertools;
 use load_cargo::{load_proc_macro, ProjectFolders};
 use proc_macro_api::ProcMacroServer;
-use project_model::{ProjectWorkspace, WorkspaceBuildScripts};
+use project_model::{ManifestPath, ProjectWorkspace, WorkspaceBuildScripts};
 use stdx::{format_to, thread::ThreadIntent};
 use triomphe::Arc;
 use vfs::{AbsPath, AbsPathBuf, ChangeKind};
@@ -204,7 +204,14 @@ impl GlobalState {
 
         self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, {
             let linked_projects = self.config.linked_or_discovered_projects();
-            let detached_files = self.config.detached_files().to_vec();
+            let detached_files: Vec<_> = self
+                .config
+                .detached_files()
+                .iter()
+                .cloned()
+                .map(ManifestPath::try_from)
+                .filter_map(Result::ok)
+                .collect();
             let cargo_config = self.config.cargo();
 
             move |sender| {
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index a03ab0031d..6e4820c5de 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -778,7 +778,8 @@ Disable project auto-discovery in favor of explicitly specified set
 of projects.
 
 Elements must be paths pointing to `Cargo.toml`,
-`rust-project.json`, or JSON objects in `rust-project.json` format.
+`rust-project.json`, `.rs` files (which will be treated as standalone files) or JSON
+objects in `rust-project.json` format.
 --
 [[rust-analyzer.lru.capacity]]rust-analyzer.lru.capacity (default: `null`)::
 +
diff --git a/editors/code/package.json b/editors/code/package.json
index 389e1b8742..352eb24229 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -1495,7 +1495,7 @@
                     "type": "boolean"
                 },
                 "rust-analyzer.linkedProjects": {
-                    "markdownDescription": "Disable project auto-discovery in favor of explicitly specified set\nof projects.\n\nElements must be paths pointing to `Cargo.toml`,\n`rust-project.json`, or JSON objects in `rust-project.json` format.",
+                    "markdownDescription": "Disable project auto-discovery in favor of explicitly specified set\nof projects.\n\nElements must be paths pointing to `Cargo.toml`,\n`rust-project.json`, `.rs` files (which will be treated as standalone files) or JSON\nobjects in `rust-project.json` format.",
                     "default": [],
                     "type": "array",
                     "items": {


Test patch:
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index fd09dff503..41351d5dc0 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -1,6 +1,7 @@
 use std::ops::Deref;
 
 use base_db::{CrateGraph, FileId, ProcMacroPaths};
+use cargo_metadata::Metadata;
 use cfg::{CfgAtom, CfgDiff};
 use expect_test::{expect_file, ExpectFile};
 use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
@@ -9,8 +10,8 @@ use serde::de::DeserializeOwned;
 use triomphe::Arc;
 
 use crate::{
-    CargoWorkspace, CfgOverrides, ProjectJson, ProjectJsonData, ProjectWorkspace, Sysroot,
-    WorkspaceBuildScripts,
+    CargoWorkspace, CfgOverrides, ManifestPath, ProjectJson, ProjectJsonData, ProjectWorkspace,
+    Sysroot, WorkspaceBuildScripts,
 };
 
 fn load_cargo(file: &str) -> (CrateGraph, ProcMacroPaths) {
@@ -21,8 +22,10 @@ fn load_cargo_with_overrides(
     file: &str,
     cfg_overrides: CfgOverrides,
 ) -> (CrateGraph, ProcMacroPaths) {
-    let meta = get_test_json_file(file);
-    let cargo_workspace = CargoWorkspace::new(meta);
+    let meta: Metadata = get_test_json_file(file);
+    let manifest_path =
+        ManifestPath::try_from(AbsPathBuf::try_from(meta.workspace_root.clone()).unwrap()).unwrap();
+    let cargo_workspace = CargoWorkspace::new(meta, manifest_path);
     let project_workspace = ProjectWorkspace::Cargo {
         cargo: cargo_workspace,
         build_scripts: WorkspaceBuildScripts::default(),
@@ -41,8 +44,10 @@ fn load_cargo_with_fake_sysroot(
     file_map: &mut FxHashMap<AbsPathBuf, FileId>,
     file: &str,
 ) -> (CrateGraph, ProcMacroPaths) {
-    let meta = get_test_json_file(file);
-    let cargo_workspace = CargoWorkspace::new(meta);
+    let meta: Metadata = get_test_json_file(file);
+    let manifest_path =
+        ManifestPath::try_from(AbsPathBuf::try_from(meta.workspace_root.clone()).unwrap()).unwrap();
+    let cargo_workspace = CargoWorkspace::new(meta, manifest_path);
     let project_workspace = ProjectWorkspace::Cargo {
         cargo: cargo_workspace,
         build_scripts: WorkspaceBuildScripts::default(),
@@ -268,9 +273,10 @@ fn smoke_test_real_sysroot_cargo() {
         return;
     }
     let file_map = &mut FxHashMap::<AbsPathBuf, FileId>::default();
-    let meta = get_test_json_file("hello-world-metadata.json");
-
-    let cargo_workspace = CargoWorkspace::new(meta);
+    let meta: Metadata = get_test_json_file("hello-world-metadata.json");
+    let manifest_path =
+        ManifestPath::try_from(AbsPathBuf::try_from(meta.workspace_root.clone()).unwrap()).unwrap();
+    let cargo_workspace = CargoWorkspace::new(meta, manifest_path);
     let sysroot = Ok(Sysroot::discover(
         AbsPath::assert(Utf8Path::new(env!("CARGO_MANIFEST_DIR"))),
         &Default::default(),
diff --git a/crates/rust-analyzer/src/cli/rustc_tests.rs b/crates/rust-analyzer/src/cli/rustc_tests.rs
index 2f9394d0ee..cd15d86aa6 100644
--- a/crates/rust-analyzer/src/cli/rustc_tests.rs
+++ b/crates/rust-analyzer/src/cli/rustc_tests.rs
@@ -10,7 +10,9 @@ use ide::{AnalysisHost, DiagnosticCode, DiagnosticsConfig};
 use itertools::Either;
 use profile::StopWatch;
 use project_model::target_data_layout::RustcDataLayoutConfig;
-use project_model::{target_data_layout, CargoConfig, ProjectWorkspace, RustLibSource, Sysroot};
+use project_model::{
+    target_data_layout, CargoConfig, ManifestPath, ProjectWorkspace, RustLibSource, Sysroot,
+};
 
 use load_cargo::{load_workspace, LoadCargoConfig, ProcMacroServerChoice};
 use rustc_hash::FxHashMap;
@@ -76,7 +78,7 @@ impl Tester {
         );
 
         let workspace = ProjectWorkspace::DetachedFile {
-            file: tmp_file,
+            file: ManifestPath::try_from(tmp_file).unwrap(),
             sysroot,
             rustc_cfg: vec![],
             toolchain: None,
diff --git a/crates/rust-analyzer/tests/crate_graph.rs b/crates/rust-analyzer/tests/crate_graph.rs
index cf38032b94..e833ea5e11 100644
--- a/crates/rust-analyzer/tests/crate_graph.rs
+++ b/crates/rust-analyzer/tests/crate_graph.rs
@@ -1,14 +1,18 @@
 use std::path::PathBuf;
 
-use project_model::{CargoWorkspace, ProjectWorkspace, Sysroot, WorkspaceBuildScripts};
+use project_model::{
+    CargoWorkspace, ManifestPath, Metadata, ProjectWorkspace, Sysroot, WorkspaceBuildScripts,
+};
 use rust_analyzer::ws_to_crate_graph;
 use rustc_hash::FxHashMap;
 use serde::de::DeserializeOwned;
 use vfs::{AbsPathBuf, FileId};
 
 fn load_cargo_with_fake_sysroot(file: &str) -> ProjectWorkspace {
-    let meta = get_test_json_file(file);
-    let cargo_workspace = CargoWorkspace::new(meta);
+    let meta: Metadata = get_test_json_file(file);
+    let manifest_path =
+        ManifestPath::try_from(AbsPathBuf::try_from(meta.workspace_root.clone()).unwrap()).unwrap();
+    let cargo_workspace = CargoWorkspace::new(meta, manifest_path);
     ProjectWorkspace::Cargo {
         cargo: cargo_workspace,
         build_scripts: WorkspaceBuildScripts::default(),
diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs
index b87f02947b..2ca86bc50a 100644
--- a/crates/rust-analyzer/tests/slow-tests/main.rs
+++ b/crates/rust-analyzer/tests/slow-tests/main.rs
@@ -150,6 +150,7 @@ use dependency2::Spam;
     )
     .with_config(serde_json::json!({
         "cargo": { "sysroot": null },
+        "detachedFiles": ["/src/lib.rs"],
     }))
     .server()
     .wait_until_workspace_is_loaded();
diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs
index f04962a7a2..cf27cc7eef 100644
--- a/crates/rust-analyzer/tests/slow-tests/support.rs
+++ b/crates/rust-analyzer/tests/slow-tests/support.rs
@@ -185,11 +185,7 @@ impl Project<'_> {
             roots,
             None,
         );
-        // TODO: don't hardcode src/lib.rs as detached file
-        let mut c = self.config;
-        let p = tmp_dir_path.join("src/lib.rs").to_string();
-        c["detachedFiles"] = serde_json::json!([p]);
-        config.update(c).expect("invalid config");
+        config.update(self.config).expect("invalid config");
         config.rediscover_workspaces();
 
         Server::new(tmp_dir.keep(), config)