about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/project-model/src/cargo_workspace.rs4
-rw-r--r--crates/project-model/src/workspace.rs99
-rw-r--r--crates/rust-analyzer/src/cli/rustc_tests.rs1
-rw-r--r--crates/rust-analyzer/src/global_state.rs12
-rw-r--r--crates/rust-analyzer/src/handlers/notification.rs6
-rw-r--r--crates/rust-analyzer/src/reload.rs14
-rw-r--r--crates/rust-analyzer/tests/slow-tests/main.rs98
-rw-r--r--crates/rust-analyzer/tests/slow-tests/support.rs20
8 files changed, 238 insertions, 16 deletions
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index fd898ffa5c3..125961210dd 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -305,6 +305,10 @@ impl CargoWorkspace {
                     .collect(),
             );
         }
+        if cargo_toml.extension().is_some_and(|x| x == "rs") {
+            // TODO: enable `+nightly` for cargo scripts
+            other_options.push("-Zscript".to_owned());
+        }
         meta.other_options(other_options);
 
         // FIXME: Fetching metadata is a slow process, as it might require
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index a5e74763d70..3263d491761 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -2,7 +2,7 @@
 //! metadata` or `rust-project.json`) into representation stored in the salsa
 //! database -- `CrateGraph`.
 
-use std::{collections::VecDeque, fmt, fs, iter, sync};
+use std::{collections::VecDeque, fmt, fs, io::BufRead, iter, sync};
 
 use anyhow::{format_err, Context};
 use base_db::{
@@ -115,9 +115,55 @@ pub enum ProjectWorkspace {
         target_layout: TargetLayoutLoadResult,
         /// A set of cfg overrides for the files.
         cfg_overrides: CfgOverrides,
+        /// Is this file a cargo script file?
+        cargo_script: Option<CargoWorkspace>,
     },
 }
 
+/// Tracks the cargo toml parts in cargo scripts, to detect if they
+/// changed and reload workspace in that case.
+pub struct CargoScriptTomls(pub FxHashMap<AbsPathBuf, String>);
+
+impl CargoScriptTomls {
+    fn extract_toml_part(p: &AbsPath) -> Option<String> {
+        let mut r = String::new();
+        let f = std::fs::File::open(p).ok()?;
+        let f = std::io::BufReader::new(f);
+        let mut started = false;
+        for line in f.lines() {
+            let line = line.ok()?;
+            if started {
+                if line.trim() == "//! ```" {
+                    return Some(r);
+                }
+                r += &line;
+            } else {
+                if line.trim() == "//! ```cargo" {
+                    started = true;
+                }
+            }
+        }
+        None
+    }
+
+    pub fn track_file(&mut self, p: AbsPathBuf) {
+        let toml = CargoScriptTomls::extract_toml_part(&p).unwrap_or_default();
+        self.0.insert(p, toml);
+    }
+
+    pub fn need_reload(&mut self, p: &AbsPath) -> bool {
+        let Some(prev) = self.0.get_mut(p) else {
+            return false; // File is not tracked
+        };
+        let next = CargoScriptTomls::extract_toml_part(p).unwrap_or_default();
+        if *prev == next {
+            return false;
+        }
+        *prev = next;
+        true
+    }
+}
+
 impl fmt::Debug for ProjectWorkspace {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         // Make sure this isn't too verbose.
@@ -174,10 +220,12 @@ impl fmt::Debug for ProjectWorkspace {
                 toolchain,
                 target_layout,
                 cfg_overrides,
+                cargo_script,
             } => f
                 .debug_struct("DetachedFiles")
                 .field("n_files", &files.len())
                 .field("sysroot", &sysroot.is_ok())
+                .field("cargo_script", &cargo_script.is_some())
                 .field("n_rustc_cfg", &rustc_cfg.len())
                 .field("toolchain", &toolchain)
                 .field("data_layout", &target_layout)
@@ -431,6 +479,7 @@ impl ProjectWorkspace {
     pub fn load_detached_files(
         detached_files: Vec<AbsPathBuf>,
         config: &CargoConfig,
+        cargo_script_tomls: &mut CargoScriptTomls,
     ) -> anyhow::Result<ProjectWorkspace> {
         let dir = detached_files
             .first()
@@ -469,6 +518,23 @@ impl ProjectWorkspace {
             None,
             &config.extra_env,
         );
+        let cargo_toml = ManifestPath::try_from(detached_files[0].clone()).unwrap();
+        let meta = CargoWorkspace::fetch_metadata(
+            &cargo_toml,
+            cargo_toml.parent(),
+            config,
+            sysroot_ref,
+            &|_| (),
+        )
+        .with_context(|| {
+            format!("Failed to read Cargo metadata from Cargo.toml file {cargo_toml}")
+        })?;
+        let cargo = CargoWorkspace::new(meta);
+
+        for file in &detached_files {
+            cargo_script_tomls.track_file(file.clone());
+        }
+
         Ok(ProjectWorkspace::DetachedFiles {
             files: detached_files,
             sysroot,
@@ -476,6 +542,7 @@ impl ProjectWorkspace {
             toolchain,
             target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
             cfg_overrides: config.cfg_overrides.clone(),
+            cargo_script: Some(cargo),
         })
     }
 
@@ -788,14 +855,27 @@ impl ProjectWorkspace {
                 toolchain: _,
                 target_layout: _,
                 cfg_overrides,
+                cargo_script,
             } => (
-                detached_files_to_crate_graph(
-                    rustc_cfg.clone(),
-                    load,
-                    files,
-                    sysroot.as_ref().ok(),
-                    cfg_overrides,
-                ),
+                if let Some(cargo) = cargo_script {
+                    cargo_to_crate_graph(
+                        load,
+                        None,
+                        cargo,
+                        sysroot.as_ref().ok(),
+                        rustc_cfg.clone(),
+                        cfg_overrides,
+                        &WorkspaceBuildScripts::default(),
+                    )
+                } else {
+                    detached_files_to_crate_graph(
+                        rustc_cfg.clone(),
+                        load,
+                        files,
+                        sysroot.as_ref().ok(),
+                        cfg_overrides,
+                    )
+                },
                 sysroot,
             ),
         };
@@ -873,6 +953,7 @@ impl ProjectWorkspace {
                     files,
                     sysroot,
                     rustc_cfg,
+                    cargo_script,
                     toolchain,
                     target_layout,
                     cfg_overrides,
@@ -881,6 +962,7 @@ impl ProjectWorkspace {
                     files: o_files,
                     sysroot: o_sysroot,
                     rustc_cfg: o_rustc_cfg,
+                    cargo_script: o_cargo_script,
                     toolchain: o_toolchain,
                     target_layout: o_target_layout,
                     cfg_overrides: o_cfg_overrides,
@@ -892,6 +974,7 @@ impl ProjectWorkspace {
                     && toolchain == o_toolchain
                     && target_layout == o_target_layout
                     && cfg_overrides == o_cfg_overrides
+                    && cargo_script == o_cargo_script
             }
             _ => false,
         }
diff --git a/crates/rust-analyzer/src/cli/rustc_tests.rs b/crates/rust-analyzer/src/cli/rustc_tests.rs
index 548dd4e70e5..d6253df1347 100644
--- a/crates/rust-analyzer/src/cli/rustc_tests.rs
+++ b/crates/rust-analyzer/src/cli/rustc_tests.rs
@@ -82,6 +82,7 @@ impl Tester {
             toolchain: None,
             target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
             cfg_overrides: Default::default(),
+            cargo_script: None,
         };
         let load_cargo_config = LoadCargoConfig {
             load_out_dirs_from_check: false,
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index ec10bc7ccda..75f2a5dc661 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::{
+    CargoScriptTomls, CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts,
+};
 use rustc_hash::{FxHashMap, FxHashSet};
 use triomphe::Arc;
 use vfs::{AnchoredPathBuf, ChangedFile, Vfs};
@@ -144,6 +146,7 @@ pub(crate) struct GlobalState {
     /// this queue should run only *after* [`GlobalState::process_changes`] has
     /// been called.
     pub(crate) deferred_task_queue: TaskQueue,
+    pub(crate) cargo_script_tomls: Arc<Mutex<CargoScriptTomls>>,
 }
 
 /// An immutable snapshot of the world's state at a point in time.
@@ -240,6 +243,7 @@ impl GlobalState {
             prime_caches_queue: OpQueue::default(),
 
             deferred_task_queue: task_queue,
+            cargo_script_tomls: Arc::new(Mutex::new(CargoScriptTomls(FxHashMap::default()))),
         };
         // Apply any required database inputs from the config.
         this.update_configuration(config);
@@ -322,7 +326,11 @@ impl GlobalState {
                     if file.is_created_or_deleted() {
                         workspace_structure_change.get_or_insert((path, false)).1 |=
                             self.crate_graph_file_dependencies.contains(vfs_path);
-                    } else if reload::should_refresh_for_change(&path, file.kind()) {
+                    } else if reload::should_refresh_for_change(
+                        &path,
+                        file.kind(),
+                        &mut self.cargo_script_tomls.lock(),
+                    ) {
                         workspace_structure_change.get_or_insert((path.clone(), false));
                     }
                 }
diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs
index 68dc2cf2e29..905cca1ee0a 100644
--- a/crates/rust-analyzer/src/handlers/notification.rs
+++ b/crates/rust-analyzer/src/handlers/notification.rs
@@ -150,7 +150,11 @@ pub(crate) fn handle_did_save_text_document(
     if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
         // Re-fetch workspaces if a workspace related file has changed
         if let Some(abs_path) = vfs_path.as_path() {
-            if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) {
+            if reload::should_refresh_for_change(
+                abs_path,
+                ChangeKind::Modify,
+                &mut state.cargo_script_tomls.lock(),
+            ) {
                 state
                     .fetch_workspaces_queue
                     .request_op(format!("workspace vfs file change saved {abs_path}"), false);
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index 00a61758f06..a61aabc8a03 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::{CargoScriptTomls, ProjectWorkspace, WorkspaceBuildScripts};
 use stdx::{format_to, thread::ThreadIntent};
 use triomphe::Arc;
 use vfs::{AbsPath, AbsPathBuf, ChangeKind};
@@ -206,6 +206,7 @@ impl GlobalState {
             let linked_projects = self.config.linked_or_discovered_projects();
             let detached_files = self.config.detached_files().to_vec();
             let cargo_config = self.config.cargo();
+            let cargo_script_tomls = self.cargo_script_tomls.clone();
 
             move |sender| {
                 let progress = {
@@ -258,6 +259,7 @@ impl GlobalState {
                     workspaces.push(project_model::ProjectWorkspace::load_detached_files(
                         detached_files,
                         &cargo_config,
+                        &mut cargo_script_tomls.lock(),
                     ));
                 }
 
@@ -758,7 +760,15 @@ pub fn ws_to_crate_graph(
     (crate_graph, proc_macro_paths, layouts, toolchains)
 }
 
-pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool {
+pub(crate) fn should_refresh_for_change(
+    path: &AbsPath,
+    change_kind: ChangeKind,
+    cargo_script_tomls: &mut CargoScriptTomls,
+) -> bool {
+    if cargo_script_tomls.need_reload(path) {
+        return true;
+    }
+
     const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
     const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];
 
diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs
index 786a80c4a68..98bae08432d 100644
--- a/crates/rust-analyzer/tests/slow-tests/main.rs
+++ b/crates/rust-analyzer/tests/slow-tests/main.rs
@@ -118,6 +118,104 @@ fn f() {
 }
 
 #[test]
+fn completes_items_from_standard_library_in_cargo_script() {
+    if skip_slow_tests() {
+        return;
+    }
+
+    let server = Project::with_fixture(
+        r#"
+//- /dependency/Cargo.toml
+[package]
+name = "dependency"
+version = "0.1.0"
+//- /dependency/src/lib.rs
+pub struct SpecialHashMap;
+//- /dependency2/Cargo.toml
+[package]
+name = "dependency2"
+version = "0.1.0"
+//- /dependency2/src/lib.rs
+pub struct SpecialHashMap2;
+//- /src/lib.rs
+#!/usr/bin/env -S cargo +nightly -Zscript
+//! ```cargo
+//! [dependencies]
+//! dependency = { path = "../dependency" }
+//! ```
+use dependency::Spam;
+use dependency2::Spam;
+"#,
+    )
+    .with_config(serde_json::json!({
+        "cargo": { "sysroot": "discover" },
+    }))
+    .server()
+    .wait_until_workspace_is_loaded();
+
+    let res = server.send_request::<Completion>(CompletionParams {
+        text_document_position: TextDocumentPositionParams::new(
+            server.doc_id("src/lib.rs"),
+            Position::new(7, 18),
+        ),
+        context: None,
+        partial_result_params: PartialResultParams::default(),
+        work_done_progress_params: WorkDoneProgressParams::default(),
+    });
+    assert!(res.to_string().contains("SpecialHashMap"));
+
+    let res = server.send_request::<Completion>(CompletionParams {
+        text_document_position: TextDocumentPositionParams::new(
+            server.doc_id("src/lib.rs"),
+            Position::new(8, 18),
+        ),
+        context: None,
+        partial_result_params: PartialResultParams::default(),
+        work_done_progress_params: WorkDoneProgressParams::default(),
+    });
+    assert!(!res.to_string().contains("SpecialHashMap"));
+
+    server.write_file_and_save(
+        "src/lib.rs",
+        r#"#!/usr/bin/env -S cargo +nightly -Zscript
+//! ```cargo
+//! [dependencies]
+//! dependency2 = { path = "../dependency2" }
+//! ```
+use dependency::Spam;
+use dependency2::Spam;
+"#
+        .to_owned(),
+    );
+
+    let server = server.wait_until_workspace_is_loaded();
+
+    std::thread::sleep(std::time::Duration::from_secs(3));
+
+    let res = server.send_request::<Completion>(CompletionParams {
+        text_document_position: TextDocumentPositionParams::new(
+            server.doc_id("src/lib.rs"),
+            Position::new(7, 18),
+        ),
+        context: None,
+        partial_result_params: PartialResultParams::default(),
+        work_done_progress_params: WorkDoneProgressParams::default(),
+    });
+    assert!(!res.to_string().contains("SpecialHashMap"));
+
+    let res = server.send_request::<Completion>(CompletionParams {
+        text_document_position: TextDocumentPositionParams::new(
+            server.doc_id("src/lib.rs"),
+            Position::new(8, 18),
+        ),
+        context: None,
+        partial_result_params: PartialResultParams::default(),
+        work_done_progress_params: WorkDoneProgressParams::default(),
+    });
+    assert!(res.to_string().contains("SpecialHashMap"));
+}
+
+#[test]
 fn test_runnables_project() {
     if skip_slow_tests() {
         return;
diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs
index c6778bf6564..74fbd2fbaac 100644
--- a/crates/rust-analyzer/tests/slow-tests/support.rs
+++ b/crates/rust-analyzer/tests/slow-tests/support.rs
@@ -125,7 +125,7 @@ impl Project<'_> {
         }
 
         let mut config = Config::new(
-            tmp_dir_path,
+            tmp_dir_path.clone(),
             lsp_types::ClientCapabilities {
                 workspace: Some(lsp_types::WorkspaceClientCapabilities {
                     did_change_watched_files: Some(
@@ -185,10 +185,14 @@ impl Project<'_> {
             roots,
             None,
         );
-        config.update(self.config).expect("invalid config");
+        // 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.rediscover_workspaces();
 
-        Server::new(tmp_dir, config)
+        Server::new(tmp_dir.keep(), config)
     }
 }
 
@@ -374,6 +378,16 @@ impl Server {
     pub(crate) fn path(&self) -> &Utf8Path {
         self.dir.path()
     }
+
+    pub(crate) fn write_file_and_save(&self, path: &str, text: String) {
+        fs::write(self.dir.path().join(path), &text).unwrap();
+        self.notification::<lsp_types::notification::DidSaveTextDocument>(
+            lsp_types::DidSaveTextDocumentParams {
+                text_document: self.doc_id(path),
+                text: Some(text),
+            },
+        )
+    }
 }
 
 impl Drop for Server {