diff --git a/.gitignore b/.gitignore index 253f8f33d7..9ca9bd48db 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ exercises/clippy/Cargo.lock .idea .vscode *.iml +rust-project.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e536d1b77a..f228ee0efb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,15 @@ dependencies = [ "libc", ] +[[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.10.3" @@ -289,9 +298,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "kernel32-sys" @@ -547,11 +556,13 @@ dependencies = [ "assert_cmd", "console 0.7.7", "glob", + "home", "indicatif", "notify", "predicates", "regex", "serde", + "serde_json", "toml", ] @@ -578,18 +589,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.129" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -598,9 +609,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 30032695c4..66dcd8e5d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,10 @@ console = "0.7.7" notify = "4.0.15" toml = "0.4.10" regex = "1.1.6" -serde = {version = "1.0.10", features = ["derive"]} +serde = { version = "1.0.133", features = ["derive"] } +glob = "0.3.0" +serde_json = "1.0.74" +home = "0.5.3" [[bin]] name = "rustlings" diff --git a/src/fix_rust_analyzer.rs b/src/fix_rust_analyzer.rs new file mode 100644 index 0000000000..f08941795a --- /dev/null +++ b/src/fix_rust_analyzer.rs @@ -0,0 +1,100 @@ +/// because `rustlings` is a special type of project where we don't have a +/// cargo.toml linking to each exercise, we need a way to tell `rust-analyzer` +/// how to parse the exercises. This functionality is built into rust-analyzer +/// by putting a `rust-project.json` at the root of the repository. This module generates +/// that file by finding the default toolchain used and looping through each exercise +/// to build the configuration in a way that allows rust-analyzer to work with the exercises. +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, + crates: Vec, +} + +#[derive(Serialize, Deserialize)] +struct Crate { + root_module: String, + edition: String, + deps: 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(), + }) + } + } + } + + /// 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> { + let glob = glob("./exercises/**/*")?; + for e in glob { + let path = e?.to_string_lossy().to_string(); + self.path_to_json(path); + } + Ok(()) + } + + /// Use `rustup` command to determine the default toolchain, if it exists + /// it will be put in RustAnalyzerProject.sysroot_src, otherwise an error will be returned + pub fn get_sysroot_src(&mut self) -> Result<(), Box> { + let mut sysroot_src = home::rustup_home()?.to_string_lossy().to_string(); + + let output = Command::new("rustup").arg("default").output()?; + + let toolchain = String::from_utf8_lossy(&output.stdout).to_string(); + + sysroot_src += "/toolchains/"; + sysroot_src += toolchain + .split_once(' ') + .ok_or("Unable to determine toolchain")? + .0; + sysroot_src += "/lib/rustlib/src/rust/library"; + println!( + "Determined toolchain to use with Rustlings: {}", + sysroot_src + ); + self.sysroot_src = sysroot_src; + Ok(()) + } +} + +#[test] +fn parses_exercises() { + let mut rust_project = RustAnalyzerProject::new(); + rust_project + .exercies_to_json() + .expect("Failed to parse exercises"); + assert_eq!(rust_project.crates.len() > 0, true); +} diff --git a/src/main.rs b/src/main.rs index 32e7bba2f6..6c12447d38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::exercise::{Exercise, ExerciseList}; +use crate::fix_rust_analyzer::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 fix_rust_analyzer; mod run; mod verify; @@ -37,6 +39,9 @@ struct Args { version: bool, #[argh(subcommand)] nested: Option, + /// skip rust-analyzer fix check + #[argh(switch, short = 'x')] + skipfix: bool, } #[derive(FromArgs, PartialEq, Debug)] @@ -128,6 +133,10 @@ fn main() { std::process::exit(1); } + if !Path::new("rust-project.json").exists() && !args.skipfix { + fix_rust_analyzer(); + } + if !rustc_exists() { println!("We cannot find `rustc`."); println!("Try running `rustc --version` to diagnose your problem."); @@ -404,3 +413,16 @@ fn rustc_exists() -> bool { .map(|status| status.success()) .unwrap_or(false) } + +fn fix_rust_analyzer() { + let mut rust_project = RustAnalyzerProject::new(); + if let Err(err) = rust_project.get_sysroot_src() { + println!("Couldn't find toolchain path for rust-analyzer: {}", &err) + } + if let Err(err) = rust_project.exercies_to_json() { + println!("Couldn't parse exercises for rust-analyzer: {}", &err) + } + if let Err(_) = rust_project.write_to_disk() { + println!("Failed to write rust-project.json to disk for rust-analyzer"); + }; +} diff --git a/tests/fixture/failure/rust-project.json b/tests/fixture/failure/rust-project.json new file mode 100644 index 0000000000..f373cd742e --- /dev/null +++ b/tests/fixture/failure/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file diff --git a/tests/fixture/state/rust-project.json b/tests/fixture/state/rust-project.json new file mode 100644 index 0000000000..f373cd742e --- /dev/null +++ b/tests/fixture/state/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file diff --git a/tests/fixture/success/rust-project.json b/tests/fixture/success/rust-project.json new file mode 100644 index 0000000000..f373cd742e --- /dev/null +++ b/tests/fixture/success/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file