Use `TempDir` for copied lockfiles
diff --git a/Cargo.lock b/Cargo.lock
index a699463..c19e847 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1679,6 +1679,7 @@
  "serde_json",
  "span",
  "stdx",
+ "temp-dir",
  "toolchain",
  "tracing",
  "triomphe",
@@ -2285,6 +2286,12 @@
 ]
 
 [[package]]
+name = "temp-dir"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
+
+[[package]]
 name = "tenthash"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3103,6 +3110,7 @@
  "proc-macro2",
  "quote",
  "stdx",
+ "time",
  "ungrammar",
  "write-json",
  "xflags",
diff --git a/Cargo.toml b/Cargo.toml
index 700c116..87202e8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -156,6 +156,7 @@
   "const_generics",
 ] }
 smol_str = "0.3.2"
+temp-dir = "0.1.16"
 text-size = "1.1.1"
 tracing = "0.1.41"
 tracing-tree = "0.4.0"
diff --git a/crates/project-model/Cargo.toml b/crates/project-model/Cargo.toml
index 27fe9f7..0dbb309 100644
--- a/crates/project-model/Cargo.toml
+++ b/crates/project-model/Cargo.toml
@@ -20,6 +20,7 @@
 serde_json.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+temp-dir.workspace = true
 tracing.workspace = true
 triomphe.workspace = true
 la-arena.workspace = true
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index daadcd9..25abb19 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -552,6 +552,7 @@
 
 pub(crate) struct FetchMetadata {
     command: cargo_metadata::MetadataCommand,
+    manifest_path: ManifestPath,
     lockfile_path: Option<Utf8PathBuf>,
     kind: &'static str,
     no_deps: bool,
@@ -655,7 +656,15 @@
         }
         .with_context(|| format!("Failed to run `{cargo_command:?}`"));
 
-        Self { command, lockfile_path, kind: config.kind, no_deps, no_deps_result, other_options }
+        Self {
+            manifest_path: cargo_toml.clone(),
+            command,
+            lockfile_path,
+            kind: config.kind,
+            no_deps,
+            no_deps_result,
+            other_options,
+        }
     }
 
     pub(crate) fn no_deps_metadata(&self) -> Option<&cargo_metadata::Metadata> {
@@ -672,18 +681,47 @@
         locked: bool,
         progress: &dyn Fn(String),
     ) -> anyhow::Result<(cargo_metadata::Metadata, Option<anyhow::Error>)> {
-        let Self { mut command, lockfile_path, kind, no_deps, no_deps_result, mut other_options } =
-            self;
+        let Self {
+            mut command,
+            manifest_path,
+            lockfile_path,
+            kind,
+            no_deps,
+            no_deps_result,
+            mut other_options,
+        } = self;
 
         if no_deps {
             return no_deps_result.map(|m| (m, None));
         }
 
         let mut using_lockfile_copy = false;
+        let mut _temp_dir_guard = None;
         // The manifest is a rust file, so this means its a script manifest
         if let Some(lockfile) = lockfile_path {
-            let target_lockfile =
-                target_dir.join("rust-analyzer").join("metadata").join(kind).join("Cargo.lock");
+            _temp_dir_guard = temp_dir::TempDir::with_prefix("rust-analyzer").ok();
+            let target_lockfile = _temp_dir_guard
+                .and_then(|tmp| tmp.path().join("Cargo.lock").try_into().ok())
+                .unwrap_or_else(|| {
+                    // When multiple workspaces share the same target dir, they might overwrite into a
+                    // single lockfile path.
+                    // See https://github.com/rust-lang/rust-analyzer/issues/20189#issuecomment-3073520255
+                    let manifest_path_hash = std::hash::BuildHasher::hash_one(
+                        &std::hash::BuildHasherDefault::<rustc_hash::FxHasher>::default(),
+                        &manifest_path,
+                    );
+                    let disambiguator = format!(
+                        "{}_{manifest_path_hash}",
+                        manifest_path.components().nth_back(1).map_or("", |c| c.as_str())
+                    );
+
+                    target_dir
+                        .join("rust-analyzer")
+                        .join("metadata")
+                        .join(kind)
+                        .join(disambiguator)
+                        .join("Cargo.lock")
+                });
             match std::fs::copy(&lockfile, &target_lockfile) {
                 Ok(_) => {
                     using_lockfile_copy = true;
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 677f29e..655da11 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -1905,7 +1905,8 @@
     meta.manifest_path(manifest);
     // `--no-deps` doesn't (over)write lockfiles as it doesn't do any package resolve.
     // So we can use it to get `target_directory` before copying lockfiles
-    let mut other_options = vec!["--no-deps".to_owned()];
+    meta.no_deps();
+    let mut other_options = vec![];
     if manifest.is_rust_manifest() {
         meta.env("RUSTC_BOOTSTRAP", "1");
         other_options.push("-Zscript".to_owned());