diff --git a/.gitignore b/.gitignore index 253f8f33..253d85bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target/ *.pdb exercises/clippy/Cargo.toml exercises/clippy/Cargo.lock +rust-project.json .idea .vscode *.iml diff --git a/Cargo.lock b/Cargo.lock index 02b78eca..6737d29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "indicatif" version = "0.16.2" @@ -229,9 +238,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "kernel32-sys" @@ -456,11 +465,13 @@ dependencies = [ "assert_cmd", "console", "glob", + "home", "indicatif", "notify", "predicates", "regex", "serde", + "serde_json", "toml", ] @@ -501,9 +512,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index befdd6e8..e2a05aae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ notify = "4.0" toml = "0.5" regex = "1.5" serde= { version = "1.0", features = ["derive"] } +serde_json = "1.0.81" +home = "0.5.3" +glob = "0.3.0" [[bin]] name = "rustlings" diff --git a/README.md b/README.md index dd96a59c..12a77810 100644 --- a/README.md +++ b/README.md @@ -126,24 +126,7 @@ After every couple of sections, there will be a quiz that'll test your knowledge ## Enabling `rust-analyzer` -`rust-analyzer` support is provided, but it depends on your editor -whether it's enabled by default. (RLS support is not provided) - -To enable `rust-analyzer`, you'll need to make Cargo build the project -with the `exercises` feature, which will automatically include the `exercises/` -subfolder in the project. The easiest way to do this is to tell your editor to -build the project with all features (the equivalent of `cargo build --all-features`). -For specific editor instructions: - -- **VSCode**: Add a `.vscode/settings.json` file with the following: -```json -{ - "rust-analyzer.cargo.features": ["exercises"] -} -``` -- **IntelliJ-based Editors**: Using the Rust plugin, everything should work - by default. -- _Missing your editor? Feel free to contribute more instructions!_ +Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise. ## Continuing On diff --git a/src/main.rs b/src/main.rs index af3dffbc..24ffbc5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::exercise::{Exercise, ExerciseList}; +use crate::project::RustAnalyzerProject; use crate::run::run; use crate::verify::verify; use argh::FromArgs; @@ -20,6 +21,7 @@ use std::time::Duration; mod ui; mod exercise; +mod project; mod run; mod verify; @@ -47,6 +49,7 @@ enum Subcommands { Run(RunArgs), Hint(HintArgs), List(ListArgs), + Lsp(LspArgs), } #[derive(FromArgs, PartialEq, Debug)] @@ -77,6 +80,12 @@ struct HintArgs { name: String, } +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "lsp")] +/// Enable rust-analyzer for exercises +struct LspArgs {} + + #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "list")] /// Lists the exercises available in Rustlings @@ -206,6 +215,25 @@ fn main() { verify(&exercises, (0, exercises.len()), verbose).unwrap_or_else(|_| std::process::exit(1)); } + Subcommands::Lsp(_subargs) => { + let mut project = RustAnalyzerProject::new(); + project + .get_sysroot_src() + .expect("Couldn't find toolchain path, do you have `rustc` installed?"); + project + .exercies_to_json() + .expect("Couldn't parse rustlings exercises files"); + + if project.crates.is_empty() { + println!("Failed find any exercises, make sure you're in the `rustlings` folder"); + } else if project.write_to_disk().is_err() { + println!("Failed to write rust-project.json to disk for rust-analyzer"); + } else { + println!("Successfully generated rust-project.json"); + println!("rust-analyzer will now parse exercises, restart your language server or editor") + } + } + Subcommands::Watch(_subargs) => match watch(&exercises, verbose) { Err(e) => { println!("Error: Could not watch your progress. Error message was {:?}.", e); @@ -224,6 +252,7 @@ fn main() { } } + fn spawn_watch_shell(failed_exercise_hint: &Arc>>, should_quit: Arc) { let failed_exercise_hint = Arc::clone(failed_exercise_hint); println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); @@ -367,6 +396,8 @@ started, here's a couple of notes about how Rustlings operates: 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, and sometimes, other learners do too so you can help each other out! +5. If you want to use `rust-analyzer` with exercises, which provides features like + autocompletion, run the command `rustlings lsp`. Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. Make sure to have your editor open!"#; diff --git a/src/project.rs b/src/project.rs new file mode 100644 index 00000000..0df00b9a --- /dev/null +++ b/src/project.rs @@ -0,0 +1,90 @@ +use glob::glob; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::process::Command; + +/// Contains the structure of resulting rust-project.json file +/// and functions to build the data required to create the file +#[derive(Serialize, Deserialize)] +pub struct RustAnalyzerProject { + sysroot_src: String, + pub crates: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct Crate { + root_module: String, + edition: String, + deps: Vec, + cfg: Vec, +} + +impl RustAnalyzerProject { + pub fn new() -> RustAnalyzerProject { + RustAnalyzerProject { + sysroot_src: String::new(), + crates: Vec::new(), + } + } + + /// Write rust-project.json to disk + pub fn write_to_disk(&self) -> Result<(), std::io::Error> { + std::fs::write( + "./rust-project.json", + serde_json::to_vec(&self).expect("Failed to serialize to JSON"), + )?; + Ok(()) + } + + /// If path contains .rs extension, add a crate to `rust-project.json` + fn path_to_json(&mut self, path: String) { + if let Some((_, ext)) = path.split_once('.') { + if ext == "rs" { + self.crates.push(Crate { + root_module: path, + edition: "2021".to_string(), + deps: Vec::new(), + // This allows rust_analyzer to work inside #[test] blocks + cfg: vec!["test".to_string()], + }) + } + } + } + + /// Parse the exercises folder for .rs files, any matches will create + /// a new `crate` in rust-project.json which allows rust-analyzer to + /// treat it like a normal binary + pub fn exercies_to_json(&mut self) -> Result<(), Box> { + for e in glob("./exercises/**/*")? { + let path = e?.to_string_lossy().to_string(); + self.path_to_json(path); + } + Ok(()) + } + + /// Use `rustc` to determine the default toolchain + pub fn get_sysroot_src(&mut self) -> Result<(), Box> { + let toolchain = Command::new("rustc") + .arg("--print") + .arg("sysroot") + .output()? + .stdout; + + let toolchain = String::from_utf8_lossy(&toolchain); + let mut whitespace_iter = toolchain.split_whitespace(); + + let toolchain = whitespace_iter.next().unwrap_or(&toolchain); + + println!("Determined toolchain: {}\n", &toolchain); + + self.sysroot_src = (std::path::Path::new(&*toolchain) + .join("lib") + .join("rustlib") + .join("src") + .join("rust") + .join("library") + .to_string_lossy()) + .to_string(); + Ok(()) + } +}