Skip to content

Commit 2f82807

Browse files
HKalbasiVeykril
authored andcommitted
Add minimal support for cargo scripts
1 parent 50bdeaa commit 2f82807

File tree

8 files changed

+238
-16
lines changed

8 files changed

+238
-16
lines changed

crates/project-model/src/cargo_workspace.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ impl CargoWorkspace {
305305
.collect(),
306306
);
307307
}
308+
if cargo_toml.extension().is_some_and(|x| x == "rs") {
309+
// TODO: enable `+nightly` for cargo scripts
310+
other_options.push("-Zscript".to_owned());
311+
}
308312
meta.other_options(other_options);
309313

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

crates/project-model/src/workspace.rs

Lines changed: 91 additions & 8 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, iter, sync};
5+
use std::{collections::VecDeque, fmt, fs, io::BufRead, iter, sync};
66

77
use anyhow::{format_err, Context};
88
use base_db::{
@@ -115,9 +115,55 @@ pub enum ProjectWorkspace {
115115
target_layout: TargetLayoutLoadResult,
116116
/// A set of cfg overrides for the files.
117117
cfg_overrides: CfgOverrides,
118+
/// Is this file a cargo script file?
119+
cargo_script: Option<CargoWorkspace>,
118120
},
119121
}
120122

123+
/// Tracks the cargo toml parts in cargo scripts, to detect if they
124+
/// changed and reload workspace in that case.
125+
pub struct CargoScriptTomls(pub FxHashMap<AbsPathBuf, String>);
126+
127+
impl CargoScriptTomls {
128+
fn extract_toml_part(p: &AbsPath) -> Option<String> {
129+
let mut r = String::new();
130+
let f = std::fs::File::open(p).ok()?;
131+
let f = std::io::BufReader::new(f);
132+
let mut started = false;
133+
for line in f.lines() {
134+
let line = line.ok()?;
135+
if started {
136+
if line.trim() == "//! ```" {
137+
return Some(r);
138+
}
139+
r += &line;
140+
} else {
141+
if line.trim() == "//! ```cargo" {
142+
started = true;
143+
}
144+
}
145+
}
146+
None
147+
}
148+
149+
pub fn track_file(&mut self, p: AbsPathBuf) {
150+
let toml = CargoScriptTomls::extract_toml_part(&p).unwrap_or_default();
151+
self.0.insert(p, toml);
152+
}
153+
154+
pub fn need_reload(&mut self, p: &AbsPath) -> bool {
155+
let Some(prev) = self.0.get_mut(p) else {
156+
return false; // File is not tracked
157+
};
158+
let next = CargoScriptTomls::extract_toml_part(p).unwrap_or_default();
159+
if *prev == next {
160+
return false;
161+
}
162+
*prev = next;
163+
true
164+
}
165+
}
166+
121167
impl fmt::Debug for ProjectWorkspace {
122168
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123169
// Make sure this isn't too verbose.
@@ -174,10 +220,12 @@ impl fmt::Debug for ProjectWorkspace {
174220
toolchain,
175221
target_layout,
176222
cfg_overrides,
223+
cargo_script,
177224
} => f
178225
.debug_struct("DetachedFiles")
179226
.field("n_files", &files.len())
180227
.field("sysroot", &sysroot.is_ok())
228+
.field("cargo_script", &cargo_script.is_some())
181229
.field("n_rustc_cfg", &rustc_cfg.len())
182230
.field("toolchain", &toolchain)
183231
.field("data_layout", &target_layout)
@@ -431,6 +479,7 @@ impl ProjectWorkspace {
431479
pub fn load_detached_files(
432480
detached_files: Vec<AbsPathBuf>,
433481
config: &CargoConfig,
482+
cargo_script_tomls: &mut CargoScriptTomls,
434483
) -> anyhow::Result<ProjectWorkspace> {
435484
let dir = detached_files
436485
.first()
@@ -469,13 +518,31 @@ impl ProjectWorkspace {
469518
None,
470519
&config.extra_env,
471520
);
521+
let cargo_toml = ManifestPath::try_from(detached_files[0].clone()).unwrap();
522+
let meta = CargoWorkspace::fetch_metadata(
523+
&cargo_toml,
524+
cargo_toml.parent(),
525+
config,
526+
sysroot_ref,
527+
&|_| (),
528+
)
529+
.with_context(|| {
530+
format!("Failed to read Cargo metadata from Cargo.toml file {cargo_toml}")
531+
})?;
532+
let cargo = CargoWorkspace::new(meta);
533+
534+
for file in &detached_files {
535+
cargo_script_tomls.track_file(file.clone());
536+
}
537+
472538
Ok(ProjectWorkspace::DetachedFiles {
473539
files: detached_files,
474540
sysroot,
475541
rustc_cfg,
476542
toolchain,
477543
target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
478544
cfg_overrides: config.cfg_overrides.clone(),
545+
cargo_script: Some(cargo),
479546
})
480547
}
481548

@@ -788,14 +855,27 @@ impl ProjectWorkspace {
788855
toolchain: _,
789856
target_layout: _,
790857
cfg_overrides,
858+
cargo_script,
791859
} => (
792-
detached_files_to_crate_graph(
793-
rustc_cfg.clone(),
794-
load,
795-
files,
796-
sysroot.as_ref().ok(),
797-
cfg_overrides,
798-
),
860+
if let Some(cargo) = cargo_script {
861+
cargo_to_crate_graph(
862+
load,
863+
None,
864+
cargo,
865+
sysroot.as_ref().ok(),
866+
rustc_cfg.clone(),
867+
cfg_overrides,
868+
&WorkspaceBuildScripts::default(),
869+
)
870+
} else {
871+
detached_files_to_crate_graph(
872+
rustc_cfg.clone(),
873+
load,
874+
files,
875+
sysroot.as_ref().ok(),
876+
cfg_overrides,
877+
)
878+
},
799879
sysroot,
800880
),
801881
};
@@ -873,6 +953,7 @@ impl ProjectWorkspace {
873953
files,
874954
sysroot,
875955
rustc_cfg,
956+
cargo_script,
876957
toolchain,
877958
target_layout,
878959
cfg_overrides,
@@ -881,6 +962,7 @@ impl ProjectWorkspace {
881962
files: o_files,
882963
sysroot: o_sysroot,
883964
rustc_cfg: o_rustc_cfg,
965+
cargo_script: o_cargo_script,
884966
toolchain: o_toolchain,
885967
target_layout: o_target_layout,
886968
cfg_overrides: o_cfg_overrides,
@@ -892,6 +974,7 @@ impl ProjectWorkspace {
892974
&& toolchain == o_toolchain
893975
&& target_layout == o_target_layout
894976
&& cfg_overrides == o_cfg_overrides
977+
&& cargo_script == o_cargo_script
895978
}
896979
_ => false,
897980
}

crates/rust-analyzer/src/cli/rustc_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ impl Tester {
8282
toolchain: None,
8383
target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
8484
cfg_overrides: Default::default(),
85+
cargo_script: None,
8586
};
8687
let load_cargo_config = LoadCargoConfig {
8788
load_out_dirs_from_check: false,

crates/rust-analyzer/src/global_state.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use parking_lot::{
1818
RwLockWriteGuard,
1919
};
2020
use proc_macro_api::ProcMacroServer;
21-
use project_model::{CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts};
21+
use project_model::{
22+
CargoScriptTomls, CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts,
23+
};
2224
use rustc_hash::{FxHashMap, FxHashSet};
2325
use triomphe::Arc;
2426
use vfs::{AnchoredPathBuf, ChangedFile, Vfs};
@@ -144,6 +146,7 @@ pub(crate) struct GlobalState {
144146
/// this queue should run only *after* [`GlobalState::process_changes`] has
145147
/// been called.
146148
pub(crate) deferred_task_queue: TaskQueue,
149+
pub(crate) cargo_script_tomls: Arc<Mutex<CargoScriptTomls>>,
147150
}
148151

149152
/// An immutable snapshot of the world's state at a point in time.
@@ -240,6 +243,7 @@ impl GlobalState {
240243
prime_caches_queue: OpQueue::default(),
241244

242245
deferred_task_queue: task_queue,
246+
cargo_script_tomls: Arc::new(Mutex::new(CargoScriptTomls(FxHashMap::default()))),
243247
};
244248
// Apply any required database inputs from the config.
245249
this.update_configuration(config);
@@ -322,7 +326,11 @@ impl GlobalState {
322326
if file.is_created_or_deleted() {
323327
workspace_structure_change.get_or_insert((path, false)).1 |=
324328
self.crate_graph_file_dependencies.contains(vfs_path);
325-
} else if reload::should_refresh_for_change(&path, file.kind()) {
329+
} else if reload::should_refresh_for_change(
330+
&path,
331+
file.kind(),
332+
&mut self.cargo_script_tomls.lock(),
333+
) {
326334
workspace_structure_change.get_or_insert((path.clone(), false));
327335
}
328336
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ pub(crate) fn handle_did_save_text_document(
150150
if let Ok(vfs_path) = from_proto::vfs_path(&params.text_document.uri) {
151151
// Re-fetch workspaces if a workspace related file has changed
152152
if let Some(abs_path) = vfs_path.as_path() {
153-
if reload::should_refresh_for_change(abs_path, ChangeKind::Modify) {
153+
if reload::should_refresh_for_change(
154+
abs_path,
155+
ChangeKind::Modify,
156+
&mut state.cargo_script_tomls.lock(),
157+
) {
154158
state
155159
.fetch_workspaces_queue
156160
.request_op(format!("workspace vfs file change saved {abs_path}"), false);

crates/rust-analyzer/src/reload.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use ide_db::{
2525
use itertools::Itertools;
2626
use load_cargo::{load_proc_macro, ProjectFolders};
2727
use proc_macro_api::ProcMacroServer;
28-
use project_model::{ProjectWorkspace, WorkspaceBuildScripts};
28+
use project_model::{CargoScriptTomls, ProjectWorkspace, WorkspaceBuildScripts};
2929
use stdx::{format_to, thread::ThreadIntent};
3030
use triomphe::Arc;
3131
use vfs::{AbsPath, AbsPathBuf, ChangeKind};
@@ -206,6 +206,7 @@ impl GlobalState {
206206
let linked_projects = self.config.linked_or_discovered_projects();
207207
let detached_files = self.config.detached_files().to_vec();
208208
let cargo_config = self.config.cargo();
209+
let cargo_script_tomls = self.cargo_script_tomls.clone();
209210

210211
move |sender| {
211212
let progress = {
@@ -258,6 +259,7 @@ impl GlobalState {
258259
workspaces.push(project_model::ProjectWorkspace::load_detached_files(
259260
detached_files,
260261
&cargo_config,
262+
&mut cargo_script_tomls.lock(),
261263
));
262264
}
263265

@@ -758,7 +760,15 @@ pub fn ws_to_crate_graph(
758760
(crate_graph, proc_macro_paths, layouts, toolchains)
759761
}
760762

761-
pub(crate) fn should_refresh_for_change(path: &AbsPath, change_kind: ChangeKind) -> bool {
763+
pub(crate) fn should_refresh_for_change(
764+
path: &AbsPath,
765+
change_kind: ChangeKind,
766+
cargo_script_tomls: &mut CargoScriptTomls,
767+
) -> bool {
768+
if cargo_script_tomls.need_reload(path) {
769+
return true;
770+
}
771+
762772
const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
763773
const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];
764774

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,104 @@ fn f() {
117117
);
118118
}
119119

120+
#[test]
121+
fn completes_items_from_standard_library_in_cargo_script() {
122+
if skip_slow_tests() {
123+
return;
124+
}
125+
126+
let server = Project::with_fixture(
127+
r#"
128+
//- /dependency/Cargo.toml
129+
[package]
130+
name = "dependency"
131+
version = "0.1.0"
132+
//- /dependency/src/lib.rs
133+
pub struct SpecialHashMap;
134+
//- /dependency2/Cargo.toml
135+
[package]
136+
name = "dependency2"
137+
version = "0.1.0"
138+
//- /dependency2/src/lib.rs
139+
pub struct SpecialHashMap2;
140+
//- /src/lib.rs
141+
#!/usr/bin/env -S cargo +nightly -Zscript
142+
//! ```cargo
143+
//! [dependencies]
144+
//! dependency = { path = "../dependency" }
145+
//! ```
146+
use dependency::Spam;
147+
use dependency2::Spam;
148+
"#,
149+
)
150+
.with_config(serde_json::json!({
151+
"cargo": { "sysroot": "discover" },
152+
}))
153+
.server()
154+
.wait_until_workspace_is_loaded();
155+
156+
let res = server.send_request::<Completion>(CompletionParams {
157+
text_document_position: TextDocumentPositionParams::new(
158+
server.doc_id("src/lib.rs"),
159+
Position::new(7, 18),
160+
),
161+
context: None,
162+
partial_result_params: PartialResultParams::default(),
163+
work_done_progress_params: WorkDoneProgressParams::default(),
164+
});
165+
assert!(res.to_string().contains("SpecialHashMap"));
166+
167+
let res = server.send_request::<Completion>(CompletionParams {
168+
text_document_position: TextDocumentPositionParams::new(
169+
server.doc_id("src/lib.rs"),
170+
Position::new(8, 18),
171+
),
172+
context: None,
173+
partial_result_params: PartialResultParams::default(),
174+
work_done_progress_params: WorkDoneProgressParams::default(),
175+
});
176+
assert!(!res.to_string().contains("SpecialHashMap"));
177+
178+
server.write_file_and_save(
179+
"src/lib.rs",
180+
r#"#!/usr/bin/env -S cargo +nightly -Zscript
181+
//! ```cargo
182+
//! [dependencies]
183+
//! dependency2 = { path = "../dependency2" }
184+
//! ```
185+
use dependency::Spam;
186+
use dependency2::Spam;
187+
"#
188+
.to_owned(),
189+
);
190+
191+
let server = server.wait_until_workspace_is_loaded();
192+
193+
std::thread::sleep(std::time::Duration::from_secs(3));
194+
195+
let res = server.send_request::<Completion>(CompletionParams {
196+
text_document_position: TextDocumentPositionParams::new(
197+
server.doc_id("src/lib.rs"),
198+
Position::new(7, 18),
199+
),
200+
context: None,
201+
partial_result_params: PartialResultParams::default(),
202+
work_done_progress_params: WorkDoneProgressParams::default(),
203+
});
204+
assert!(!res.to_string().contains("SpecialHashMap"));
205+
206+
let res = server.send_request::<Completion>(CompletionParams {
207+
text_document_position: TextDocumentPositionParams::new(
208+
server.doc_id("src/lib.rs"),
209+
Position::new(8, 18),
210+
),
211+
context: None,
212+
partial_result_params: PartialResultParams::default(),
213+
work_done_progress_params: WorkDoneProgressParams::default(),
214+
});
215+
assert!(res.to_string().contains("SpecialHashMap"));
216+
}
217+
120218
#[test]
121219
fn test_runnables_project() {
122220
if skip_slow_tests() {

0 commit comments

Comments
 (0)