Skip to content

Commit fa67bb5

Browse files
committed
Add minimal support for cargo scripts
1 parent 2fbe69d commit fa67bb5

File tree

8 files changed

+257
-23
lines changed

8 files changed

+257
-23
lines changed

crates/project-model/src/cargo_workspace.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ impl CargoWorkspace {
276276
.collect(),
277277
);
278278
}
279+
if cargo_toml.extension().is_some_and(|x| x == "rs") {
280+
// TODO: enable `+nightly` for cargo scripts
281+
other_options.push("-Zscript".to_owned());
282+
}
279283
meta.other_options(other_options);
280284

281285
// FIXME: Fetching metadata is a slow process, as it might require

crates/project-model/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub use crate::{
5050
manifest_path::ManifestPath,
5151
project_json::{ProjectJson, ProjectJsonData},
5252
sysroot::Sysroot,
53-
workspace::{CfgOverrides, PackageRoot, ProjectWorkspace},
53+
workspace::{CargoScriptTomls, CfgOverrides, PackageRoot, ProjectWorkspace},
5454
};
5555

5656
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]

crates/project-model/src/workspace.rs

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! metadata` or `rust-project.json`) into representation stored in the salsa
33
//! database -- `CrateGraph`.
44
5-
use std::{collections::VecDeque, fmt, fs, process::Command, sync};
5+
use std::{collections::VecDeque, fmt, fs, io::BufRead, process::Command, sync};
66

77
use anyhow::{format_err, Context};
88
use base_db::{
@@ -96,9 +96,54 @@ pub enum ProjectWorkspace {
9696
/// Holds cfg flags for the current target. We get those by running
9797
/// `rustc --print cfg`.
9898
rustc_cfg: Vec<CfgFlag>,
99+
cargo_script: Option<CargoWorkspace>,
99100
},
100101
}
101102

103+
/// Tracks the cargo toml parts in cargo scripts, to detect if they
104+
/// changed and reload workspace in that case.
105+
pub struct CargoScriptTomls(pub FxHashMap<AbsPathBuf, String>);
106+
107+
impl CargoScriptTomls {
108+
fn extract_toml_part(p: &AbsPath) -> Option<String> {
109+
let mut r = String::new();
110+
let f = std::fs::File::open(p).ok()?;
111+
let f = std::io::BufReader::new(f);
112+
let mut started = false;
113+
for line in f.lines() {
114+
let line = line.ok()?;
115+
if started {
116+
if line.trim() == "//! ```" {
117+
return Some(r);
118+
}
119+
r += &line;
120+
} else {
121+
if line.trim() == "//! ```cargo" {
122+
started = true;
123+
}
124+
}
125+
}
126+
None
127+
}
128+
129+
pub fn track_file(&mut self, p: AbsPathBuf) {
130+
let toml = CargoScriptTomls::extract_toml_part(&p).unwrap_or_default();
131+
self.0.insert(p, toml);
132+
}
133+
134+
pub fn need_reload(&mut self, p: &AbsPath) -> bool {
135+
let Some(prev) = self.0.get_mut(p) else {
136+
return false; // File is not tracked
137+
};
138+
let next = CargoScriptTomls::extract_toml_part(p).unwrap_or_default();
139+
if *prev == next {
140+
return false;
141+
}
142+
*prev = next;
143+
true
144+
}
145+
}
146+
102147
impl fmt::Debug for ProjectWorkspace {
103148
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104149
// Make sure this isn't too verbose.
@@ -136,7 +181,7 @@ impl fmt::Debug for ProjectWorkspace {
136181
debug_struct.field("n_rustc_cfg", &rustc_cfg.len());
137182
debug_struct.finish()
138183
}
139-
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => f
184+
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg, cargo_script: _ } => f
140185
.debug_struct("DetachedFiles")
141186
.field("n_files", &files.len())
142187
.field("sysroot", &sysroot.is_ok())
@@ -342,6 +387,7 @@ impl ProjectWorkspace {
342387
pub fn load_detached_files(
343388
detached_files: Vec<AbsPathBuf>,
344389
config: &CargoConfig,
390+
cargo_script_tomls: &mut CargoScriptTomls,
345391
) -> anyhow::Result<ProjectWorkspace> {
346392
let sysroot = match &config.sysroot {
347393
Some(RustLibSource::Path(path)) => Sysroot::with_sysroot_dir(path.clone())
@@ -361,7 +407,24 @@ impl ProjectWorkspace {
361407
tracing::info!(src_root = %sysroot.src_root(), root = %sysroot.root(), "Using sysroot");
362408
}
363409
let rustc_cfg = rustc_cfg::get(None, None, &Default::default());
364-
Ok(ProjectWorkspace::DetachedFiles { files: detached_files, sysroot, rustc_cfg })
410+
let cargo_toml = ManifestPath::try_from(detached_files[0].clone()).unwrap();
411+
let meta =
412+
CargoWorkspace::fetch_metadata(&cargo_toml, cargo_toml.parent(), config, &|_| ())
413+
.with_context(|| {
414+
format!("Failed to read Cargo metadata from Cargo.toml file {cargo_toml}")
415+
})?;
416+
let cargo = CargoWorkspace::new(meta);
417+
418+
for file in &detached_files {
419+
cargo_script_tomls.track_file(file.clone());
420+
}
421+
422+
Ok(ProjectWorkspace::DetachedFiles {
423+
files: detached_files,
424+
sysroot,
425+
rustc_cfg,
426+
cargo_script: Some(cargo),
427+
})
365428
}
366429

367430
/// Runs the build scripts for this [`ProjectWorkspace`].
@@ -628,14 +691,29 @@ impl ProjectWorkspace {
628691
},
629692
toolchain.as_ref().and_then(|it| ReleaseChannel::from_str(it.pre.as_str())),
630693
),
631-
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg } => {
632-
detached_files_to_crate_graph(
633-
rustc_cfg.clone(),
634-
load,
635-
files,
636-
sysroot.as_ref().ok(),
637-
Err("detached file projects have no target layout set".into()),
638-
)
694+
ProjectWorkspace::DetachedFiles { files, sysroot, rustc_cfg, cargo_script } => {
695+
if let Some(cargo) = cargo_script {
696+
cargo_to_crate_graph(
697+
load,
698+
None,
699+
cargo,
700+
sysroot.as_ref().ok(),
701+
rustc_cfg.clone(),
702+
&CfgOverrides::default(),
703+
None,
704+
&WorkspaceBuildScripts::default(),
705+
Err("detached file projects have no target layout set".into()),
706+
None,
707+
)
708+
} else {
709+
detached_files_to_crate_graph(
710+
rustc_cfg.clone(),
711+
load,
712+
files,
713+
sysroot.as_ref().ok(),
714+
Err("detached file projects have no target layout set".into()),
715+
)
716+
}
639717
}
640718
};
641719
if crate_graph.patch_cfg_if() {
@@ -692,9 +770,19 @@ impl ProjectWorkspace {
692770
&& toolchain == o_toolchain
693771
}
694772
(
695-
Self::DetachedFiles { files, sysroot, rustc_cfg },
696-
Self::DetachedFiles { files: o_files, sysroot: o_sysroot, rustc_cfg: o_rustc_cfg },
697-
) => files == o_files && sysroot == o_sysroot && rustc_cfg == o_rustc_cfg,
773+
Self::DetachedFiles { files, sysroot, rustc_cfg, cargo_script },
774+
Self::DetachedFiles {
775+
files: o_files,
776+
sysroot: o_sysroot,
777+
rustc_cfg: o_rustc_cfg,
778+
cargo_script: o_cargo_script,
779+
},
780+
) => {
781+
files == o_files
782+
&& sysroot == o_sysroot
783+
&& rustc_cfg == o_rustc_cfg
784+
&& cargo_script == o_cargo_script
785+
}
698786
_ => false,
699787
}
700788
}

crates/rust-analyzer/src/global_state.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use lsp_types::{SemanticTokens, Url};
1414
use nohash_hasher::IntMap;
1515
use parking_lot::{Mutex, RwLock};
1616
use proc_macro_api::ProcMacroServer;
17-
use project_model::{CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts};
17+
use project_model::{
18+
CargoScriptTomls, CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts,
19+
};
1820
use rustc_hash::{FxHashMap, FxHashSet};
1921
use triomphe::Arc;
2022
use vfs::AnchoredPathBuf;
@@ -121,6 +123,7 @@ pub(crate) struct GlobalState {
121123
OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>,
122124
pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, bool>,
123125
pub(crate) prime_caches_queue: OpQueue,
126+
pub(crate) cargo_script_tomls: Arc<Mutex<CargoScriptTomls>>,
124127
}
125128

126129
/// An immutable snapshot of the world's state at a point in time.
@@ -204,6 +207,7 @@ impl GlobalState {
204207
fetch_proc_macros_queue: OpQueue::default(),
205208

206209
prime_caches_queue: OpQueue::default(),
210+
cargo_script_tomls: Arc::new(Mutex::new(CargoScriptTomls(FxHashMap::default()))),
207211
};
208212
// Apply any required database inputs from the config.
209213
this.update_configuration(config);
@@ -276,7 +280,11 @@ impl GlobalState {
276280
let vfs_path = &vfs.file_path(file.file_id);
277281
if let Some(path) = vfs_path.as_path() {
278282
let path = path.to_path_buf();
279-
if reload::should_refresh_for_change(&path, file.change_kind) {
283+
if reload::should_refresh_for_change(
284+
&path,
285+
file.change_kind,
286+
&mut self.cargo_script_tomls.lock(),
287+
) {
280288
workspace_structure_change = Some((path.clone(), false));
281289
}
282290
if file.is_created_or_deleted() {

crates/rust-analyzer/src/handlers/notification.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ pub(crate) fn handle_did_save_text_document(
124124
if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
125125
// Re-fetch workspaces if a workspace related file has changed
126126
if let Some(abs_path) = vfs_path.as_path() {
127-
if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) {
127+
if reload::should_refresh_for_change(
128+
abs_path,
129+
ChangeKind::Modify,
130+
&mut state.cargo_script_tomls.lock(),
131+
) {
128132
state
129133
.fetch_workspaces_queue
130134
.request_op(format!("DidSaveTextDocument {abs_path}"), false);

crates/rust-analyzer/src/reload.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use ide_db::{
2323
};
2424
use load_cargo::{load_proc_macro, ProjectFolders};
2525
use proc_macro_api::ProcMacroServer;
26-
use project_model::{ProjectWorkspace, WorkspaceBuildScripts};
26+
use project_model::{CargoScriptTomls, ProjectWorkspace, WorkspaceBuildScripts};
2727
use rustc_hash::FxHashSet;
2828
use stdx::{format_to, thread::ThreadIntent};
2929
use triomphe::Arc;
@@ -189,6 +189,7 @@ impl GlobalState {
189189
let linked_projects = self.config.linked_projects();
190190
let detached_files = self.config.detached_files().to_vec();
191191
let cargo_config = self.config.cargo();
192+
let cargo_script_tomls = self.cargo_script_tomls.clone();
192193

193194
move |sender| {
194195
let progress = {
@@ -245,6 +246,7 @@ impl GlobalState {
245246
workspaces.push(project_model::ProjectWorkspace::load_detached_files(
246247
detached_files,
247248
&cargo_config,
249+
&mut cargo_script_tomls.lock(),
248250
));
249251
}
250252

@@ -622,7 +624,15 @@ impl GlobalState {
622624
}
623625
}
624626

625-
pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool {
627+
pub(crate) fn should_refresh_for_change(
628+
path: &AbsPath,
629+
change_kind: ChangeKind,
630+
cargo_script_tomls: &mut CargoScriptTomls,
631+
) -> bool {
632+
if cargo_script_tomls.need_reload(path) {
633+
return true;
634+
}
635+
626636
const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
627637
const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];
628638

crates/rust-analyzer/tests/slow-tests/main.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,112 @@ use std::collections::Spam;
7676
assert!(res.to_string().contains("HashMap"));
7777
}
7878

79+
#[test]
80+
fn completes_items_from_standard_library_in_cargo_script() {
81+
if skip_slow_tests() {
82+
return;
83+
}
84+
85+
let server = Project::with_fixture(
86+
r#"
87+
//- /dependency/Cargo.toml
88+
[package]
89+
name = "dependency"
90+
version = "0.1.0"
91+
92+
//- /dependency/src/lib.rs
93+
pub struct SpecialHashMap;
94+
95+
//- /dependency2/Cargo.toml
96+
[package]
97+
name = "dependency2"
98+
version = "0.1.0"
99+
100+
//- /dependency2/src/lib.rs
101+
pub struct SpecialHashMap2;
102+
103+
//- /src/lib.rs
104+
#!/usr/bin/env -S cargo +nightly -Zscript
105+
106+
//! ```cargo
107+
//! [dependencies]
108+
//! dependency = { path = "../dependency" }
109+
//! ```
110+
111+
use dependency::Spam;
112+
use dependency2::Spam;
113+
"#,
114+
)
115+
.with_config(serde_json::json!({
116+
"cargo": { "sysroot": "discover" },
117+
}))
118+
.server()
119+
.wait_until_workspace_is_loaded();
120+
121+
let res = server.send_request::<Completion>(CompletionParams {
122+
text_document_position: TextDocumentPositionParams::new(
123+
server.doc_id("src/lib.rs"),
124+
Position::new(7, 18),
125+
),
126+
context: None,
127+
partial_result_params: PartialResultParams::default(),
128+
work_done_progress_params: WorkDoneProgressParams::default(),
129+
});
130+
assert!(res.to_string().contains("SpecialHashMap"));
131+
132+
let res = server.send_request::<Completion>(CompletionParams {
133+
text_document_position: TextDocumentPositionParams::new(
134+
server.doc_id("src/lib.rs"),
135+
Position::new(8, 18),
136+
),
137+
context: None,
138+
partial_result_params: PartialResultParams::default(),
139+
work_done_progress_params: WorkDoneProgressParams::default(),
140+
});
141+
assert!(!res.to_string().contains("SpecialHashMap"));
142+
143+
server.write_file_and_save(
144+
"src/lib.rs",
145+
r#"#!/usr/bin/env -S cargo +nightly -Zscript
146+
147+
//! ```cargo
148+
//! [dependencies]
149+
//! dependency2 = { path = "../dependency2" }
150+
//! ```
151+
152+
use dependency::Spam;
153+
use dependency2::Spam;
154+
"#
155+
.to_owned(),
156+
);
157+
158+
let server = server.wait_until_workspace_is_loaded();
159+
160+
std::thread::sleep(std::time::Duration::from_secs(3));
161+
162+
let res = server.send_request::<Completion>(CompletionParams {
163+
text_document_position: TextDocumentPositionParams::new(
164+
server.doc_id("src/lib.rs"),
165+
Position::new(7, 18),
166+
),
167+
context: None,
168+
partial_result_params: PartialResultParams::default(),
169+
work_done_progress_params: WorkDoneProgressParams::default(),
170+
});
171+
assert!(!res.to_string().contains("SpecialHashMap"));
172+
173+
let res = server.send_request::<Completion>(CompletionParams {
174+
text_document_position: TextDocumentPositionParams::new(
175+
server.doc_id("src/lib.rs"),
176+
Position::new(8, 18),
177+
),
178+
context: None,
179+
partial_result_params: PartialResultParams::default(),
180+
work_done_progress_params: WorkDoneProgressParams::default(),
181+
});
182+
assert!(res.to_string().contains("SpecialHashMap"));
183+
}
184+
79185
#[test]
80186
fn test_runnables_project() {
81187
if skip_slow_tests() {

0 commit comments

Comments
 (0)