From b3aef377beacb09d8efff5a59376edc7fae7766c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 03:33:14 +0100 Subject: [PATCH 01/11] Use a custom capacity for the JSON buffer --- src/project.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/project.rs b/src/project.rs index 00fc304d..93f941dd 100644 --- a/src/project.rs +++ b/src/project.rs @@ -31,10 +31,12 @@ impl RustAnalyzerProject { /// 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"), - )?; + // Using the capacity 2^14 = 16384 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(16384); + serde_json::to_writer(&mut buf, &self).expect("Failed to serialize to JSON"); + std::fs::write("rust-project.json", buf)?; Ok(()) } From efa9f5704853acda6874725004b480d720683faf Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 03:46:56 +0100 Subject: [PATCH 02/11] Add anyhow --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/main.rs | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3950c476..270051ee 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" @@ -525,6 +531,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" name = "rustlings" version = "5.6.1" dependencies = [ + "anyhow", "assert_cmd", "clap", "console", diff --git a/Cargo.toml b/Cargo.toml index 218b7990..d7b5a096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ authors = [ edition = "2021" [dependencies] +anyhow = "1.0.81" clap = { version = "4.5.2", features = ["derive"] } console = "0.15.8" glob = "0.3.0" diff --git a/src/main.rs b/src/main.rs index a06f0c56..4a4f2198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use crate::exercise::{Exercise, ExerciseList}; use crate::project::RustAnalyzerProject; 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}; @@ -84,7 +85,7 @@ enum Subcommands { Lsp, } -fn main() { +fn main() -> Result<()> { let args = Args::parse(); if args.command.is_none() { @@ -243,6 +244,8 @@ fn main() { } }, } + + Ok(()) } fn spawn_watch_shell( From 51712cc19f97972f470c4d8791974f8eaba095d1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 03:49:10 +0100 Subject: [PATCH 03/11] Merge get_sysroot_src into the constructor --- src/main.rs | 5 +--- src/project.rs | 77 +++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4a4f2198..4ce0b30d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,10 +204,7 @@ fn main() -> Result<()> { } Subcommands::Lsp => { - let mut project = RustAnalyzerProject::new(); - project - .get_sysroot_src() - .expect("Couldn't find toolchain path, do you have `rustc` installed?"); + let mut project = RustAnalyzerProject::build()?; project .exercises_to_json() .expect("Couldn't parse rustlings exercises files"); diff --git a/src/project.rs b/src/project.rs index 93f941dd..a7414d1f 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Context, Result}; use glob::glob; use serde::{Deserialize, Serialize}; use std::env; @@ -22,11 +23,44 @@ pub struct Crate { } impl RustAnalyzerProject { - pub fn new() -> RustAnalyzerProject { - RustAnalyzerProject { - sysroot_src: String::new(), - crates: Vec::new(), + pub fn build() -> Result { + // check if RUST_SRC_PATH is set + if let Ok(sysroot_src) = env::var("RUST_SRC_PATH") { + return Ok(Self { + sysroot_src, + crates: Vec::new(), + }); } + + let toolchain = Command::new("rustc") + .arg("--print") + .arg("sysroot") + .output() + .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? + .stdout; + + 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(sysroot_src) = Path::new(toolchain) + .join("lib") + .join("rustlib") + .join("src") + .join("rust") + .join("library") + .into_os_string() + .into_string() + else { + bail!("The sysroot path is invalid UTF8"); + }; + + Ok(Self { + sysroot_src, + crates: Vec::new(), + }) } /// Write rust-project.json to disk @@ -66,39 +100,4 @@ impl RustAnalyzerProject { } 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(()); - } - - let toolchain = Command::new("rustc") - .arg("--print") - .arg("sysroot") - .output()? - .stdout; - - let toolchain = String::from_utf8(toolchain)?; - 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; - - Ok(()) - } } From d095a307ddbdef1f67e89320491c76a1bed1c8eb Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 03:59:21 +0100 Subject: [PATCH 04/11] Avoid allocations on every call to Path::join --- src/project.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/project.rs b/src/project.rs index a7414d1f..c017aa22 100644 --- a/src/project.rs +++ b/src/project.rs @@ -3,7 +3,7 @@ use glob::glob; use serde::{Deserialize, Serialize}; use std::env; use std::error::Error; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; /// Contains the structure of resulting rust-project.json file @@ -45,15 +45,9 @@ impl RustAnalyzerProject { println!("Determined toolchain: {toolchain}\n"); - let Ok(sysroot_src) = Path::new(toolchain) - .join("lib") - .join("rustlib") - .join("src") - .join("rust") - .join("library") - .into_os_string() - .into_string() - else { + let mut sysroot_src = PathBuf::with_capacity(256); + sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); + let Ok(sysroot_src) = sysroot_src.into_os_string().into_string() else { bail!("The sysroot path is invalid UTF8"); }; From b932ed1f672532e7dccf6cd23f6b9895c24a4de7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 17:14:41 +0100 Subject: [PATCH 05/11] Don't capture stderr --- src/project.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/project.rs b/src/project.rs index c017aa22..1f42d4eb 100644 --- a/src/project.rs +++ b/src/project.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::env; use std::error::Error; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Stdio}; /// Contains the structure of resulting rust-project.json file /// and functions to build the data required to create the file @@ -35,6 +35,7 @@ impl RustAnalyzerProject { let toolchain = Command::new("rustc") .arg("--print") .arg("sysroot") + .stderr(Stdio::inherit()) .output() .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? .stdout; From 87e55ccffde51b08be7d90ab53f1bb2462efa85a Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 22:20:00 +0100 Subject: [PATCH 06/11] Use the parsed exercises instead of glob --- Cargo.toml | 1 - src/main.rs | 2 +- src/project.rs | 35 +++++++++++++---------------------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7b5a096..ef499473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ edition = "2021" anyhow = "1.0.81" clap = { version = "4.5.2", features = ["derive"] } console = "0.15.8" -glob = "0.3.0" home = "0.5.9" indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" diff --git a/src/main.rs b/src/main.rs index 4ce0b30d..803e2f8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -206,7 +206,7 @@ fn main() -> Result<()> { Subcommands::Lsp => { let mut project = RustAnalyzerProject::build()?; project - .exercises_to_json() + .exercises_to_json(exercises) .expect("Couldn't parse rustlings exercises files"); if project.crates.is_empty() { diff --git a/src/project.rs b/src/project.rs index 1f42d4eb..534aab09 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,11 +1,12 @@ use anyhow::{bail, Context, Result}; -use glob::glob; use serde::{Deserialize, Serialize}; use std::env; use std::error::Error; 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)] @@ -69,30 +70,20 @@ impl RustAnalyzerProject { 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?)?; - } + pub fn exercises_to_json(&mut self, exercises: Vec) -> Result<(), Box> { + self.crates = exercises + .into_iter() + .map(|exercise| Crate { + root_module: exercise.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()], + }) + .collect(); Ok(()) } } From f5135ae4df96ee018896d667f3dffa187c959193 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 22:29:33 +0100 Subject: [PATCH 07/11] Remove unneeded check if crates is empty --- src/main.rs | 4 +--- src/project.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 803e2f8f..1f260ab7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -209,9 +209,7 @@ fn main() -> Result<()> { .exercises_to_json(exercises) .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() { + 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"); diff --git a/src/project.rs b/src/project.rs index 534aab09..835a951a 100644 --- a/src/project.rs +++ b/src/project.rs @@ -12,7 +12,7 @@ use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] pub struct RustAnalyzerProject { sysroot_src: String, - pub crates: Vec, + crates: Vec, } #[derive(Serialize, Deserialize)] From a5ba44bd6a939a720cc600e06785bea98baabc37 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 22:30:16 +0100 Subject: [PATCH 08/11] RustAnalyzerProject is not deserialized --- src/project.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/project.rs b/src/project.rs index 835a951a..347ca461 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::env; use std::error::Error; use std::path::PathBuf; @@ -9,13 +9,13 @@ 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)] +#[derive(Serialize)] pub struct RustAnalyzerProject { sysroot_src: String, crates: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize)] pub struct Crate { root_module: String, edition: String, From 8d3ec24c11654d668ef1e1638a7770ec8beadfb7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 22:41:14 +0100 Subject: [PATCH 09/11] Optimize the serialized data types --- src/project.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/project.rs b/src/project.rs index 347ca461..54cffe12 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use serde::Serialize; use std::env; use std::error::Error; @@ -11,24 +11,25 @@ use crate::exercise::Exercise; /// and functions to build the data required to create the file #[derive(Serialize)] pub struct RustAnalyzerProject { - sysroot_src: String, + sysroot_src: PathBuf, crates: Vec, } #[derive(Serialize)] -pub struct Crate { - root_module: String, - edition: String, - deps: Vec, - cfg: Vec, +struct Crate { + root_module: PathBuf, + edition: &'static str, + // Not used, but required in the JSON file. + deps: Vec<()>, + cfg: [&'static str; 1], } impl RustAnalyzerProject { pub fn build() -> Result { // check if RUST_SRC_PATH is set - if let Ok(sysroot_src) = env::var("RUST_SRC_PATH") { + if let Some(path) = env::var_os("RUST_SRC_PATH") { return Ok(Self { - sysroot_src, + sysroot_src: PathBuf::from(path), crates: Vec::new(), }); } @@ -49,9 +50,6 @@ impl RustAnalyzerProject { let mut sysroot_src = PathBuf::with_capacity(256); sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - let Ok(sysroot_src) = sysroot_src.into_os_string().into_string() else { - bail!("The sysroot path is invalid UTF8"); - }; Ok(Self { sysroot_src, @@ -77,11 +75,11 @@ impl RustAnalyzerProject { self.crates = exercises .into_iter() .map(|exercise| Crate { - root_module: exercise.path.display().to_string(), - edition: "2021".to_string(), + root_module: exercise.path, + edition: "2021", deps: Vec::new(), // This allows rust_analyzer to work inside #[test] blocks - cfg: vec!["test".to_string()], + cfg: ["test"], }) .collect(); Ok(()) From 8ddbf9635d21a4c0306bd31cca5c4077693ca917 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 23:01:56 +0100 Subject: [PATCH 10/11] Add write_project_json --- src/main.rs | 11 +++------ src/project.rs | 63 +++++++++++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1f260ab7..46aaf1f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ 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; @@ -204,13 +204,8 @@ fn main() -> Result<()> { } Subcommands::Lsp => { - let mut project = RustAnalyzerProject::build()?; - project - .exercises_to_json(exercises) - .expect("Couldn't parse rustlings exercises files"); - - 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") diff --git a/src/project.rs b/src/project.rs index 54cffe12..acf011d3 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,7 +1,6 @@ use anyhow::{Context, Result}; use serde::Serialize; use std::env; -use std::error::Error; use std::path::PathBuf; use std::process::{Command, Stdio}; @@ -10,7 +9,7 @@ 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)] -pub struct RustAnalyzerProject { +struct RustAnalyzerProject { sysroot_src: PathBuf, crates: Vec, } @@ -25,12 +24,22 @@ struct Crate { } impl RustAnalyzerProject { - pub fn build() -> Result { - // check if RUST_SRC_PATH is set + 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(); + if let Some(path) = env::var_os("RUST_SRC_PATH") { return Ok(Self { sysroot_src: PathBuf::from(path), - crates: Vec::new(), + crates, }); } @@ -53,35 +62,21 @@ impl RustAnalyzerProject { Ok(Self { sysroot_src, - crates: Vec::new(), + crates, }) } - - /// Write rust-project.json to disk - pub fn write_to_disk(&self) -> Result<(), std::io::Error> { - // Using the capacity 2^14 = 16384 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(16384); - serde_json::to_writer(&mut buf, &self).expect("Failed to serialize to JSON"); - std::fs::write("rust-project.json", buf)?; - 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, exercises: Vec) -> Result<(), Box> { - self.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(); - Ok(()) - } +} + +/// 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(()) } From a158c77d81f2b2870385f70b63511588ed6912ff Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 25 Mar 2024 23:21:14 +0100 Subject: [PATCH 11/11] Add comment --- src/project.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/project.rs b/src/project.rs index acf011d3..0f56de96 100644 --- a/src/project.rs +++ b/src/project.rs @@ -20,6 +20,8 @@ struct Crate { 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], } @@ -31,7 +33,7 @@ impl RustAnalyzerProject { root_module: exercise.path, edition: "2021", deps: Vec::new(), - // This allows rust_analyzer to work inside #[test] blocks + // This allows rust_analyzer to work inside `#[test]` blocks cfg: ["test"], }) .collect(); @@ -54,7 +56,6 @@ impl RustAnalyzerProject { let toolchain = String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); let mut sysroot_src = PathBuf::with_capacity(256);