diff --git a/Cargo.lock b/Cargo.lock index 2cb0b9f..11880b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + [[package]] name = "assert_cmd" version = "2.0.14" @@ -560,6 +566,7 @@ dependencies = [ name = "rustlings" version = "5.6.1" dependencies = [ + "anyhow", "assert_cmd", "clap", "console", diff --git a/Cargo.toml b/Cargo.toml index 89366a0..36e1123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ authors = [ edition = "2021" [dependencies] +anyhow = "1.0.81" clap = { version = "4.5.2", features = ["derive"] } console = "0.15.8" -glob = "0.3.0" indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" serde_json = "1.0.114" diff --git a/src/main.rs b/src/main.rs index a513e71..8f73dbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use crate::exercise::{Exercise, ExerciseList}; -use crate::project::RustAnalyzerProject; +use crate::project::write_project_json; use crate::run::{reset, run}; use crate::verify::verify; +use anyhow::Result; use clap::{Parser, Subcommand}; use console::Emoji; use notify_debouncer_mini::notify::{self, RecursiveMode}; @@ -85,7 +86,7 @@ enum Subcommands { Lsp, } -fn main() { +fn main() -> Result<()> { let args = Args::parse(); if args.command.is_none() { @@ -218,18 +219,8 @@ fn main() { } Subcommands::Lsp => { - let mut project = RustAnalyzerProject::new(); - project - .get_sysroot_src() - .expect("Couldn't find toolchain path, do you have `rustc` installed?"); - project - .exercises_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"); + if let Err(e) = write_project_json(exercises) { + println!("Failed to write rust-project.json to disk for rust-analyzer: {e}"); } else { println!("Successfully generated rust-project.json"); println!("rust-analyzer will now parse exercises, restart your language server or editor"); @@ -255,6 +246,8 @@ fn main() { } }, } + + Ok(()) } fn spawn_watch_shell( diff --git a/src/project.rs b/src/project.rs index 00fc304..0f56de9 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,102 +1,83 @@ -use glob::glob; -use serde::{Deserialize, Serialize}; +use anyhow::{Context, Result}; +use serde::Serialize; use std::env; -use std::error::Error; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use crate::exercise::Exercise; /// 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)] +struct RustAnalyzerProject { + sysroot_src: PathBuf, + crates: Vec, } -#[derive(Serialize, Deserialize)] -pub struct Crate { - root_module: String, - edition: String, - deps: Vec, - cfg: Vec, +#[derive(Serialize)] +struct Crate { + root_module: PathBuf, + edition: &'static str, + // Not used, but required in the JSON file. + deps: Vec<()>, + // Only `test` is used for all crates. + // Therefore, an array is used instead of a `Vec`. + cfg: [&'static str; 1], } impl RustAnalyzerProject { - pub fn new() -> RustAnalyzerProject { - RustAnalyzerProject { - sysroot_src: String::new(), - crates: Vec::new(), - } - } + fn build(exercises: Vec) -> Result { + let crates = exercises + .into_iter() + .map(|exercise| Crate { + root_module: exercise.path, + edition: "2021", + deps: Vec::new(), + // This allows rust_analyzer to work inside `#[test]` blocks + cfg: ["test"], + }) + .collect(); - /// 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: PathBuf) -> Result<(), Box> { - if let Some(ext) = path.extension() { - if ext == "rs" { - self.crates.push(Crate { - root_module: path.display().to_string(), - edition: "2021".to_string(), - deps: Vec::new(), - // This allows rust_analyzer to work inside #[test] blocks - cfg: vec!["test".to_string()], - }) - } - } - - Ok(()) - } - - /// 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 exercises_to_json(&mut self) -> Result<(), Box> { - for path in glob("./exercises/**/*")? { - self.path_to_json(path?)?; - } - Ok(()) - } - - /// Use `rustc` to determine the default toolchain - pub fn get_sysroot_src(&mut self) -> Result<(), Box> { - // check if RUST_SRC_PATH is set - if let Ok(path) = env::var("RUST_SRC_PATH") { - self.sysroot_src = path; - return Ok(()); + if let Some(path) = env::var_os("RUST_SRC_PATH") { + return Ok(Self { + sysroot_src: PathBuf::from(path), + crates, + }); } let toolchain = Command::new("rustc") .arg("--print") .arg("sysroot") - .output()? + .stderr(Stdio::inherit()) + .output() + .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? .stdout; - let toolchain = String::from_utf8(toolchain)?; + let toolchain = + String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); - let Ok(path) = Path::new(toolchain) - .join("lib") - .join("rustlib") - .join("src") - .join("rust") - .join("library") - .into_os_string() - .into_string() - else { - return Err("The sysroot path is invalid UTF8".into()); - }; - self.sysroot_src = path; + let mut sysroot_src = PathBuf::with_capacity(256); + sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - Ok(()) + Ok(Self { + sysroot_src, + crates, + }) } } + +/// Write `rust-project.json` to disk. +pub fn write_project_json(exercises: Vec) -> Result<()> { + let content = RustAnalyzerProject::build(exercises)?; + + // Using the capacity 2^14 since the file length in bytes is higher than 2^13. + // The final length is not known exactly because it depends on the user's sysroot path, + // the current number of exercises etc. + let mut buf = Vec::with_capacity(1 << 14); + serde_json::to_writer(&mut buf, &content)?; + std::fs::write("rust-project.json", buf)?; + + Ok(()) +}