From c2455bc676dcdf3628a529cb1193e2f48fdbd195 Mon Sep 17 00:00:00 2001 From: Oleksii Khilkevych Date: Sun, 7 Sep 2025 15:28:12 +0200 Subject: [PATCH 01/78] Added rustlings in Ukrainian --- website/content/community-exercises/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/content/community-exercises/index.md b/website/content/community-exercises/index.md index 0f713d7c..8766783a 100644 --- a/website/content/community-exercises/index.md +++ b/website/content/community-exercises/index.md @@ -6,6 +6,7 @@ title = "Community Exercises" - πŸ‡―πŸ‡΅ [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises. - πŸ‡¨πŸ‡³ [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises. +- πŸ‡ΊπŸ‡¦ [Rustlings in Ukrainian](https://github.com/noroutine/rustlings-ua): Translation of the Rustlings exercises in Ukrainian. > You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises. From c6c6d272324a2cae4b4d6f4a145c43c180a3bcd8 Mon Sep 17 00:00:00 2001 From: Lev Krikken Date: Tue, 25 Nov 2025 05:41:16 +0100 Subject: [PATCH 02/78] Remove redundant error conversion functions, use enum constructors instead --- exercises/13_error_handling/errors6.rs | 11 +---------- solutions/13_error_handling/errors6.rs | 16 +++------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index b1995e03..73055c79 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -19,15 +19,6 @@ enum ParsePosNonzeroError { ParseInt(ParseIntError), } -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> Self { - Self::Creation(err) - } - - // TODO: Add another error conversion function here. - // fn from_parse_int(???) -> Self { ??? } -} - #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); @@ -44,7 +35,7 @@ impl PositiveNonzeroInteger { // TODO: change this to return an appropriate error instead of panicking // when `parse()` returns an error. let x: i64 = s.parse().unwrap(); - Self::new(x).map_err(ParsePosNonzeroError::from_creation) + Self::new(x).map_err(ParsePosNonzeroError::Creation) } } diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index ce18073a..cb49fc5d 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -19,16 +19,6 @@ enum ParsePosNonzeroError { ParseInt(ParseIntError), } -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> Self { - Self::Creation(err) - } - - fn from_parse_int(err: ParseIntError) -> Self { - Self::ParseInt(err) - } -} - // As an alternative solution, implementing the `From` trait allows for the // automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError` // using the `?` operator, without the need to call `map_err`. @@ -59,9 +49,9 @@ impl PositiveNonzeroInteger { fn parse(s: &str) -> Result { // Return an appropriate error instead of panicking when `parse()` // returns an error. - let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?; - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Self::new(x).map_err(ParsePosNonzeroError::from_creation) + let x: i64 = s.parse().map_err(ParsePosNonzeroError::ParseInt)?; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Self::new(x).map_err(ParsePosNonzeroError::Creation) } } From 0bed579a4bbca0b788cef986da3cd5117f8cf94f Mon Sep 17 00:00:00 2001 From: Eugen Date: Sat, 29 Nov 2025 19:24:55 +0900 Subject: [PATCH 03/78] try_from_into.rs: Improve slice implementation Using pattern matching, we can reduce four bound checks to just one. --- solutions/23_conversions/try_from_into.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/try_from_into.rs index ee802eb0..15257cf2 100644 --- a/solutions/23_conversions/try_from_into.rs +++ b/solutions/23_conversions/try_from_into.rs @@ -52,13 +52,12 @@ impl TryFrom<&[i16]> for Color { type Error = IntoColorError; fn try_from(slice: &[i16]) -> Result { - // Check the length. - if slice.len() != 3 { - return Err(IntoColorError::BadLen); + if let &[red, green, blue] = slice { + // Reuse the implementation for a tuple. + Self::try_from((red, green, blue)) + } else { + Err(IntoColorError::BadLen) } - - // Reuse the implementation for a tuple. - Self::try_from((slice[0], slice[1], slice[2])) } } From 1ebb4d25a62199a402e00ec4a95a9df4211c5daf Mon Sep 17 00:00:00 2001 From: Jatin Sanghvi <20547963+JatinSanghvi@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:17:47 +0530 Subject: [PATCH 04/78] Update solution files to match exercise files --- solutions/09_strings/strings4.rs | 2 +- solutions/11_hashmaps/hashmaps1.rs | 2 +- solutions/12_options/options1.rs | 26 +++++++++++++------------- solutions/13_error_handling/errors5.rs | 2 +- solutions/16_lifetimes/lifetimes2.rs | 4 ++-- solutions/21_macros/macros3.rs | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 3c69b976..087b0386 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -19,7 +19,7 @@ fn main() { // `.into()` converts a type into an expected type. // If it is called where `String` is expected, it will convert `&str` to `String`. string("nice weather".into()); - // But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed. + // But if it is called where `&str` is expected, then `&str` is kept as `&str` since no conversion is needed. // If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion. #[allow(clippy::useless_conversion)] string_slice("nice weather".into()); diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs index 3a787c43..0a654b88 100644 --- a/solutions/11_hashmaps/hashmaps1.rs +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -1,7 +1,7 @@ // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that // particular fruit is in the basket. You have to put at least 3 different -// types of fruits (e.g apple, banana, mango) in the basket and the total count +// types of fruits (e.g. apple, banana, mango) in the basket and the total count // of all the fruits should be at least 5. use std::collections::HashMap; diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs index 4d615dd6..a8e6457d 100644 --- a/solutions/12_options/options1.rs +++ b/solutions/12_options/options1.rs @@ -1,8 +1,8 @@ -// This function returns how much icecream there is left in the fridge. +// This function returns how much ice cream there is left in the fridge. // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, -// someone eats it all, so no icecream is left (value 0). Return `None` if +// someone eats it all, so no ice cream is left (value 0). Return `None` if // `hour_of_day` is higher than 23. -fn maybe_icecream(hour_of_day: u16) -> Option { +fn maybe_ice_cream(hour_of_day: u16) -> Option { match hour_of_day { 0..=21 => Some(5), 22..=23 => Some(0), @@ -21,19 +21,19 @@ mod tests { #[test] fn raw_value() { // Using `unwrap` is fine in a test. - let icecreams = maybe_icecream(12).unwrap(); + let ice_creams = maybe_ice_cream(12).unwrap(); - assert_eq!(icecreams, 5); + assert_eq!(ice_creams, 5); } #[test] - fn check_icecream() { - assert_eq!(maybe_icecream(0), Some(5)); - assert_eq!(maybe_icecream(9), Some(5)); - assert_eq!(maybe_icecream(18), Some(5)); - assert_eq!(maybe_icecream(22), Some(0)); - assert_eq!(maybe_icecream(23), Some(0)); - assert_eq!(maybe_icecream(24), None); - assert_eq!(maybe_icecream(25), None); + fn check_ice_cream() { + assert_eq!(maybe_ice_cream(0), Some(5)); + assert_eq!(maybe_ice_cream(9), Some(5)); + assert_eq!(maybe_ice_cream(18), Some(5)); + assert_eq!(maybe_ice_cream(22), Some(0)); + assert_eq!(maybe_ice_cream(23), Some(0)); + assert_eq!(maybe_ice_cream(24), None); + assert_eq!(maybe_ice_cream(25), None); } } diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index c1424eee..93ab3b9d 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -6,7 +6,7 @@ // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The `Box` is declared as of type `Box` where +// trait. To do so, the `Box` is declared as of type `Box` where // `Trait` is the trait the compiler looks for on any value used in that // context. For this exercise, that context is the potential errors which // can be returned in a `Result`. diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs index 3ca49093..412c6021 100644 --- a/solutions/16_lifetimes/lifetimes2.rs +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -4,7 +4,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { fn main() { let string1 = String::from("long string is long"); - // Solution1: You can move `strings2` out of the inner block so that it is + // Solution 1: You can move `strings2` out of the inner block so that it is // not dropped before the print statement. let string2 = String::from("xyz"); let result; @@ -21,7 +21,7 @@ fn main() { { let string2 = String::from("xyz"); result = longest(&string1, &string2); - // Solution2: You can move the print statement into the inner block so + // Solution 2: You can move the print statement into the inner block so // that it is executed before `string2` is dropped. println!("The longest string is '{result}'"); // `string2` dropped here (end of the inner scope). diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs index df35be4d..9d574f89 100644 --- a/solutions/21_macros/macros3.rs +++ b/solutions/21_macros/macros3.rs @@ -1,4 +1,4 @@ -// Added the attribute `macro_use` attribute. +// Added the `macro_use` attribute. #[macro_use] mod macros { macro_rules! my_macro { From 45f789114bae670245cddaf234d442e8a5c4a20c Mon Sep 17 00:00:00 2001 From: Rod Elias Date: Mon, 12 Jan 2026 00:22:59 -0300 Subject: [PATCH 05/78] chore: minor improvements --- exercises/09_strings/README.md | 2 +- rustlings-macros/info.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/09_strings/README.md b/exercises/09_strings/README.md index fa2104cc..a3d22f03 100644 --- a/exercises/09_strings/README.md +++ b/exercises/09_strings/README.md @@ -1,6 +1,6 @@ # Strings -Rust has two string types, a string slice (`&str`) and an owned string (`String`). +Rust has two string types: a string slice (`&str`) and an owned string (`String`). We're not going to dictate when you should use which one, but we'll show you how to identify and create them, as well as use them. diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ca3ecf1f..e42b0f26 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -21,7 +21,7 @@ get started, here are some notes about how Rustlings operates: final_message = """ We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, don't hesitate to report them on Github. +If you noticed any issues, don't hesitate to report them on GitHub. You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: From 1b47fd97c02524b74096f293249f223a5856556c Mon Sep 17 00:00:00 2001 From: Padraic Slattery Date: Thu, 22 Jan 2026 14:13:29 +0100 Subject: [PATCH 06/78] chore: Update outdated GitHub Actions versions --- .github/workflows/rust.yml | 8 ++++---- .github/workflows/website.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0317f351..0d130904 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,13 +19,13 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clippy run: cargo clippy -- --deny warnings fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: rustfmt run: cargo fmt --all --check test: @@ -34,14 +34,14 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test run: cargo test --workspace dev-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: rustlings dev check run: cargo dev check --require-solutions diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 936cd562..d7513581 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -13,7 +13,7 @@ jobs: working-directory: website runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install TailwindCSS run: npm install - name: Build CSS From de695c46f370f7628608c6efbe610c8b8bc5d44c Mon Sep 17 00:00:00 2001 From: Dipan Chakraborty Date: Tue, 10 Feb 2026 19:00:41 +0530 Subject: [PATCH 07/78] docs: add str and String documentation links --- exercises/09_strings/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/09_strings/README.md b/exercises/09_strings/README.md index fa2104cc..58b50e1e 100644 --- a/exercises/09_strings/README.md +++ b/exercises/09_strings/README.md @@ -6,4 +6,6 @@ to identify and create them, as well as use them. ## Further information -- [Strings](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [Strings (Rust Book)](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [`str` methods](https://doc.rust-lang.org/std/primitive.str.html) +- [`String` methods](https://doc.rust-lang.org/std/string/struct.String.html) From 3a00274335be26c82c4045e7ec7154a79380b410 Mon Sep 17 00:00:00 2001 From: "cheoleon (pstor)" <26815428+eoncheole@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:44:49 +0900 Subject: [PATCH 08/78] docs: add Korean Rustlings to community exercises list --- website/content/community-exercises/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/content/community-exercises/index.md b/website/content/community-exercises/index.md index 0f713d7c..b87d02c8 100644 --- a/website/content/community-exercises/index.md +++ b/website/content/community-exercises/index.md @@ -6,6 +6,7 @@ title = "Community Exercises" - πŸ‡―πŸ‡΅ [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises. - πŸ‡¨πŸ‡³ [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises. +- πŸ‡°πŸ‡· [Korean Rustlings](https://github.com/eoncheole/rustlings-kr): A Korean translation of the Rustlings exercises. > You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises. From 4817abcc1476e2d57965d19095d24ff805ba851c Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Thu, 12 Feb 2026 11:56:25 +0100 Subject: [PATCH 09/78] Mention struct-like variants in enums2 hint instead of anonymous structs --- rustlings-macros/info.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ca3ecf1f..dd1f04be 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -440,7 +440,7 @@ dir = "08_enums" test = false hint = """ You can create enumerations that have different variants with different types -such as anonymous structs, structs, a single string, tuples, no data, etc.""" +such as struct-like variants, regular structs, a single string, tuples, no data, etc.""" [[exercises]] name = "enums3" From 13564207cb5eaa2781c79e562cee7ab47b39d564 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 20 Nov 2025 12:56:58 +0100 Subject: [PATCH 10/78] Update deps --- Cargo.lock | 600 +++++++++++++++++++++--------------- Cargo.toml | 12 +- rustlings-macros/Cargo.toml | 2 +- 3 files changed, 366 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f883653f..c2c15813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -34,35 +34,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" @@ -72,21 +66,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.45" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -94,9 +88,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -106,9 +100,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -118,9 +112,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -134,7 +128,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "crossterm_winapi", "document-features", "mio", @@ -156,9 +150,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -171,12 +165,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -185,6 +179,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -196,14 +196,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", + "wasip3", ] [[package]] @@ -211,6 +212,15 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -219,13 +229,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indexmap" -version = "2.10.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -234,7 +252,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -250,15 +268,15 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "kqueue" @@ -281,55 +299,60 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.175" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -338,7 +361,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -352,9 +375,12 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "once_cell" @@ -364,15 +390,15 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -380,31 +406,41 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -417,24 +453,24 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", ] [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -462,12 +498,6 @@ dependencies = [ "toml", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -484,19 +514,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.219" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -505,23 +551,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -536,9 +583,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -547,10 +594,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -568,9 +616,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -579,25 +627,25 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "toml" -version = "0.9.5" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", "toml_datetime", "toml_parser", @@ -607,33 +655,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -658,12 +712,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -684,11 +781,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -699,18 +796,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -718,149 +806,179 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.9.2", + "wit-bindgen-rust-macro", ] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 4469b281..25415ce4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and rust-version = "1.88" [workspace.dependencies] -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1", features = ["derive"] } toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } [package] @@ -45,12 +45,12 @@ include = [ ] [dependencies] -anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } +anyhow = "1" +clap = { version = "4", features = ["derive"] } crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] } -notify = "8.0" +notify = "8" rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } -serde_json = "1.0" +serde_json = "1" serde.workspace = true toml.workspace = true @@ -58,7 +58,7 @@ toml.workspace = true rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] -tempfile = "3.21" +tempfile = "3" [profile.release] panic = "abort" diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 5df648b2..5d123b51 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -16,7 +16,7 @@ include = [ proc-macro = true [dependencies] -quote = "1.0" +quote = "1" serde.workspace = true toml.workspace = true From 0cbcb8964c434066826b65633f2b208d69c334d3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 15:55:07 +0100 Subject: [PATCH 11/78] Borrow deserialized values --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- rustlings-macros/src/lib.rs | 13 +++++++------ src/app_state.rs | 25 ++++++++++++------------- src/cargo_toml.rs | 14 +++++++------- src/dev/check.rs | 6 +++--- src/embedded.rs | 11 ++++++----- src/info_file.rs | 21 +++++++++++---------- src/init.rs | 9 +++++---- src/main.rs | 2 +- 10 files changed, 57 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2c15813..c690e819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.0.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] diff --git a/Cargo.toml b/Cargo.toml index 25415ce4..068a3f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ rust-version = "1.88" [workspace.dependencies] serde = { version = "1", features = ["derive"] } -toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } +toml = { version = "1", default-features = false, features = ["std", "parse", "serde"] } [package] name = "rustlings" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index b20c6f1d..db758d59 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -3,14 +3,15 @@ use quote::quote; use serde::Deserialize; #[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: String, +struct ExerciseInfo<'a> { + name: &'a str, + dir: &'a str, } #[derive(Deserialize)] -struct InfoFile { - exercises: Vec, +struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[proc_macro] @@ -37,7 +38,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { continue; } - dirs.push(exercise.dir.as_str()); + dirs.push(exercise.dir); *dir_ind = dirs.len() - 1; } diff --git a/src/app_state.rs b/src/app_state.rs index d654d042..71a4fbbf 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -54,7 +54,7 @@ pub struct AppState { exercises: Vec, // Caches the number of done exercises to avoid iterating over all exercises every time. n_done: u16, - final_message: String, + final_message: &'static str, state_file: File, // Preallocated buffer for reading and writing the state file. file_buf: Vec, @@ -66,7 +66,7 @@ pub struct AppState { impl AppState { pub fn new( exercise_infos: Vec, - final_message: String, + final_message: &'static str, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -87,34 +87,33 @@ impl AppState { // Leaking is not a problem because the `AppState` instance lives until // the end of the program. let path = exercise_info.path().leak(); - let name = exercise_info.name.leak(); - let dir = exercise_info.dir.map(|dir| &*dir.leak()); - let hint = exercise_info.hint.leak().trim_ascii(); + let hint = exercise_info.hint.trim_ascii(); let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; - if let Some(dir) = dir { + if let Some(dir) = exercise_info.dir { canonical_path = String::with_capacity( - 2 + dir_canonical_path.len() + dir.len() + name.len(), + 2 + dir_canonical_path.len() + dir.len() + exercise_info.name.len(), ); canonical_path.push_str(dir_canonical_path); canonical_path.push_str(MAIN_SEPARATOR_STR); canonical_path.push_str(dir); } else { - canonical_path = - String::with_capacity(1 + dir_canonical_path.len() + name.len()); + canonical_path = String::with_capacity( + 1 + dir_canonical_path.len() + exercise_info.name.len(), + ); canonical_path.push_str(dir_canonical_path); } canonical_path.push_str(MAIN_SEPARATOR_STR); - canonical_path.push_str(name); + canonical_path.push_str(exercise_info.name); canonical_path.push_str(".rs"); canonical_path }); Exercise { - dir, - name, + dir: exercise_info.dir, + name: exercise_info.name, path, canonical_path, test: exercise_info.test, @@ -616,7 +615,7 @@ mod tests { current_exercise_ind: 0, exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], n_done: 0, - final_message: String::new(), + final_message: "", state_file: tempfile::tempfile().unwrap(), file_buf: Vec::new(), official_exercises: true, diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index ce0dfd0c..9297da82 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -38,7 +38,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"exercises/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -56,7 +56,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"solutions/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -106,19 +106,19 @@ mod tests { fn test_bins() { let exercise_infos = [ ExerciseInfo { - name: String::from("1"), + name: "1", dir: None, test: true, strict_clippy: true, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ExerciseInfo { - name: String::from("2"), - dir: Some(String::from("d")), + name: "2", + dir: Some("d"), test: false, strict_clippy: false, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ]; diff --git a/src/dev/check.rs b/src/dev/check.rs index f7111063..16d04563 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -63,7 +63,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { - let name = exercise_info.name.as_str(); + let name = exercise_info.name; if name.is_empty() { bail!("Found an empty exercise name in `info.toml`"); } @@ -76,7 +76,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { bail!("Char `{c}` in the exercise name `{name}` is not allowed"); } - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { if dir.is_empty() { bail!("The exercise `{name}` has an empty dir name in `info.toml`"); } @@ -214,7 +214,7 @@ fn check_exercises_unsolved( Some( thread::Builder::new() .spawn(|| exercise_info.run_exercise(None, cmd_runner)) - .map(|handle| (exercise_info.name.as_str(), handle)), + .map(|handle| (exercise_info.name, handle)), ) }) .collect::, _>>() diff --git a/src/embedded.rs b/src/embedded.rs index 61a5f581..bee4119c 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -85,7 +85,7 @@ impl EmbeddedFiles { exercise_path.truncate(prefix.len()); exercise_path.push_str(dir.name); exercise_path.push('/'); - exercise_path.push_str(&exercise_info.name); + exercise_path.push_str(exercise_info.name); exercise_path.push_str(".rs"); fs::write(&exercise_path, exercise_files.exercise) @@ -141,13 +141,14 @@ mod tests { use super::*; #[derive(Deserialize)] - struct ExerciseInfo { - dir: String, + struct ExerciseInfo<'a> { + dir: &'a str, } #[derive(Deserialize)] - struct InfoFile { - exercises: Vec, + struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[test] diff --git a/src/info_file.rs b/src/info_file.rs index 04e5d644..8a90fcca 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -8,9 +8,9 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; #[derive(Deserialize)] pub struct ExerciseInfo { /// Exercise's unique name. - pub name: String, + pub name: &'static str, /// Exercise's directory name inside the `exercises/` directory. - pub dir: Option, + pub dir: Option<&'static str>, /// Run `cargo test` on the exercise. #[serde(default = "default_true")] pub test: bool, @@ -18,7 +18,7 @@ pub struct ExerciseInfo { #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. - pub hint: String, + pub hint: &'static str, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. #[serde(default)] pub skip_check_unsolved: bool, @@ -31,7 +31,7 @@ const fn default_true() -> bool { impl ExerciseInfo { /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { - let mut path = if let Some(dir) = &self.dir { + let mut path = if let Some(dir) = self.dir { // 14 = 10 + 1 + 3 // exercises/ + / + .rs let mut path = String::with_capacity(14 + dir.len() + self.name.len()); @@ -47,7 +47,7 @@ impl ExerciseInfo { path }; - path.push_str(&self.name); + path.push_str(self.name); path.push_str(".rs"); path @@ -57,12 +57,12 @@ impl ExerciseInfo { impl RunnableExercise for ExerciseInfo { #[inline] fn name(&self) -> &str { - &self.name + self.name } #[inline] fn dir(&self) -> Option<&str> { - self.dir.as_deref() + self.dir } #[inline] @@ -82,9 +82,9 @@ pub struct InfoFile { /// For possible breaking changes in the future for community exercises. pub format_version: u8, /// Shown to users when starting with the exercises. - pub welcome_message: Option, + pub welcome_message: Option<&'static str>, /// Shown to users after finishing all exercises. - pub final_message: Option, + pub final_message: Option<&'static str>, /// List of all exercises. pub exercises: Vec, } @@ -95,7 +95,8 @@ impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { - Ok(file_content) => toml::de::from_str::(&file_content) + // Leaking is fine since `InfoFile` is used until the end of the program. + Ok(file_content) => toml::de::from_str::(file_content.leak()) .context("Failed to parse the `info.toml` file")?, Err(e) => { if e.kind() == ErrorKind::NotFound { diff --git a/src/init.rs b/src/init.rs index 16ea35e8..9ef211e1 100644 --- a/src/init.rs +++ b/src/init.rs @@ -8,7 +8,7 @@ use std::{ env::set_current_dir, fs::{self, create_dir}, io::{self, Write}, - path::{Path, PathBuf}, + path::Path, process::{Command, Stdio}, }; @@ -18,8 +18,9 @@ use crate::{ }; #[derive(Deserialize)] -struct CargoLocateProject { - root: PathBuf, +struct CargoLocateProject<'a> { + #[serde(borrow)] + root: &'a Path, } pub fn init() -> Result<()> { @@ -72,7 +73,7 @@ pub fn init() -> Result<()> { )? .root; - let workspace_manifest_content = fs::read_to_string(&workspace_manifest) + let workspace_manifest_content = fs::read_to_string(workspace_manifest) .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?; if !workspace_manifest_content.contains("[workspace]") && !workspace_manifest_content.contains("workspace.") diff --git a/src/main.rs b/src/main.rs index ffd2dfa7..b541b68f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,7 +128,7 @@ fn main() -> Result { None } else { // For the notify event handler thread. - // Leaking is not a problem because the slice lives until the end of the program. + // Leaking is fine since the slice is used until the end of the program. Some( &*app_state .exercises() From 2512701e2fc902273d4e8e36447ef8f6b6842cbc Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:05:53 +0100 Subject: [PATCH 12/78] Keep exercise path owned --- src/app_state.rs | 20 +++++++------------- src/exercise.rs | 6 +++--- src/info_file.rs | 2 +- src/list/state.rs | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 71a4fbbf..765425a9 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -83,12 +83,6 @@ impl AppState { let mut exercises = exercise_infos .into_iter() .map(|exercise_info| { - // Leaking to be able to borrow in the watch mode `Table`. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - let path = exercise_info.path().leak(); - let hint = exercise_info.hint.trim_ascii(); - let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; if let Some(dir) = exercise_info.dir { @@ -114,11 +108,11 @@ impl AppState { Exercise { dir: exercise_info.dir, name: exercise_info.name, - path, + path: exercise_info.path(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, - hint, + hint: exercise_info.hint.trim_ascii(), // Updated below. done: false, } @@ -342,12 +336,12 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&'static str> { + pub fn reset_current_exercise(&mut self) -> Result<&str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; + self.reset(self.current_exercise_ind, &exercise.path)?; - Ok(exercise.path) + Ok(&exercise.path) } // Reset the exercise by index and return its name. @@ -358,7 +352,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, exercise.path)?; + self.reset(exercise_ind, &exercise.path)?; Ok(exercise.name) } @@ -600,7 +594,7 @@ mod tests { Exercise { dir: None, name: "0", - path: "exercises/0.rs", + path: String::from("exercises/0.rs"), canonical_path: None, test: false, strict_clippy: false, diff --git a/src/exercise.rs b/src/exercise.rs index a0596b5b..ee1f42f9 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,7 @@ pub struct Exercise { pub dir: Option<&'static str>, pub name: &'static str, /// Path of the exercise file starting with the `exercises/` directory. - pub path: &'static str, + pub path: String, pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, @@ -85,9 +85,9 @@ impl Exercise { ) -> io::Result<()> { file_path(writer, Color::Blue, |writer| { if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { - terminal_file_link(writer, self.path, canonical_path) + terminal_file_link(writer, &self.path, canonical_path) } else { - writer.write_str(self.path) + writer.write_str(&self.path) } }) } diff --git a/src/info_file.rs b/src/info_file.rs index 8a90fcca..23df0d16 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -95,7 +95,7 @@ impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { - // Leaking is fine since `InfoFile` is used until the end of the program. + // Leaking is fine since the info file is used until the end of the program. Ok(file_content) => toml::de::from_str::(file_content.leak()) .context("Failed to parse the `info.toml` file")?, Err(e) => { diff --git a/src/list/state.rs b/src/list/state.rs index 4fd1301d..55ccb4c9 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -366,11 +366,11 @@ impl<'a> ListState<'a> { let exercise_ind = self.selected_to_exercise_ind(selected)?; let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?; - self.update_rows(); write!( self.message, "The exercise `{exercise_name}` has been reset", )?; + self.update_rows(); Ok(()) } From 9011d349876bcbd3088bcc06013e4af32bb9d955 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:12:08 +0100 Subject: [PATCH 13/78] Swap name and dir to stay consistent with the info file --- src/app_state.rs | 4 ++-- src/exercise.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 765425a9..a566e14b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -106,8 +106,8 @@ impl AppState { }); Exercise { - dir: exercise_info.dir, name: exercise_info.name, + dir: exercise_info.dir, path: exercise_info.path(), canonical_path, test: exercise_info.test, @@ -592,8 +592,8 @@ mod tests { fn dummy_exercise() -> Exercise { Exercise { - dir: None, name: "0", + dir: None, path: String::from("exercises/0.rs"), canonical_path: None, test: false, diff --git a/src/exercise.rs b/src/exercise.rs index ee1f42f9..c07a94e8 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -66,8 +66,8 @@ fn run_bin( /// See `info_file::ExerciseInfo` pub struct Exercise { - pub dir: Option<&'static str>, pub name: &'static str, + pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. pub path: String, pub canonical_path: Option, From 8738518699ac4eb9b010d28a342108a0ac0707a4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:20:40 +0100 Subject: [PATCH 14/78] Use `rustlings_dir` when deleting the temporary dir before recreating it --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 9ef211e1..971b0e51 100644 --- a/src/init.rs +++ b/src/init.rs @@ -107,7 +107,7 @@ pub fn init() -> Result<()> { } stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?; - fs::remove_dir_all("rustlings") + fs::remove_dir_all(rustlings_dir) .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; } else { From aaf8cad7782beb57197ccb9edd04e5d18100ba21 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:20:40 +0100 Subject: [PATCH 15/78] Add backtrace to CI --- .github/workflows/rust.yml | 2 +- tests/integration_tests.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0d130904..828afa1a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +37,7 @@ jobs: - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test - run: cargo test --workspace + run: RUST_BACKTRACE=1 cargo test --workspace dev-check: runs-on: ubuntu-latest steps: diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb3a084b..bd8ef544 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -38,6 +38,7 @@ impl<'a> Cmd<'a> { self } + #[track_caller] fn assert(&self, success: bool) { let rustlings_bin = { let mut path = env::current_exe().unwrap(); @@ -87,11 +88,13 @@ impl<'a> Cmd<'a> { } #[inline] + #[track_caller] fn success(&self) { self.assert(true); } #[inline] + #[track_caller] fn fail(&self) { self.assert(false); } From e91647b02345fbd8c4cc4e7b536e63719b5b2be8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 16:26:23 +0100 Subject: [PATCH 16/78] Add RUSTBACKTRACE as env --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 828afa1a..9a7ae2c0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +37,9 @@ jobs: - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - name: cargo test - run: RUST_BACKTRACE=1 cargo test --workspace + env: + RUST_BACKTRACE: 1 + run: cargo test --workspace dev-check: runs-on: ubuntu-latest steps: From c163bfe563cec427b4b311ef681c6127b042df13 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 26 Feb 2026 17:35:44 +0100 Subject: [PATCH 17/78] Improve error messages if tests fail --- tests/integration_tests.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bd8ef544..520429d0 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -61,30 +61,32 @@ impl<'a> Cmd<'a> { cmd.args(self.args).stdin(Stdio::null()); - let status = match self.output { - None => cmd - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .unwrap(), + let output = match self.output { + None => cmd.output().unwrap(), Some(FullStdout(stdout)) => { let output = cmd.stderr(Stdio::null()).output().unwrap(); assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); - output.status + output } Some(PartialStdout(stdout)) => { let output = cmd.stderr(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); - output.status + output } Some(PartialStderr(stderr)) => { let output = cmd.stdout(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); - output.status + output } }; - assert_eq!(status.success(), success, "{cmd:?}"); + assert_eq!( + output.status.success(), + success, + "{cmd:?}\n\nstdout:\n{}\nstderr:\n{}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap(), + ); } #[inline] From 7e5793b64228248cdd3c1fe250f89badff3c9418 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 17:39:00 +0100 Subject: [PATCH 18/78] Remove \r on Windows --- rustlings-macros/src/lib.rs | 12 ++++++++++-- src/info_file.rs | 15 +++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index db758d59..40eebc16 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -16,8 +16,16 @@ struct InfoFile<'a> { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let info_file = include_str!("../info.toml"); - let exercises = toml::de::from_str::(info_file) + // Remove `\r` on Windows + let info_file = String::from_utf8( + include_bytes!("../info.toml") + .iter() + .copied() + .filter(|c| *c != b'\r') + .collect(), + ) + .expect("Failed to parse `info.toml` as UTF8"); + let exercises = toml::de::from_str::(&info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/info_file.rs b/src/info_file.rs index 23df0d16..13206fd8 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -94,10 +94,17 @@ impl InfoFile { /// Community exercises: Parse the `info.toml` file in the current directory. pub fn parse() -> Result { // Read a local `info.toml` if it exists. - let slf = match fs::read_to_string("info.toml") { - // Leaking is fine since the info file is used until the end of the program. - Ok(file_content) => toml::de::from_str::(file_content.leak()) - .context("Failed to parse the `info.toml` file")?, + let slf = match fs::read("info.toml") { + Ok(file_content) => { + // Remove `\r` on Windows. + // Leaking is fine since the info file is used until the end of the program. + let file_content = + String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect()) + .context("Failed to parse `info.toml` as UTF8")? + .leak(); + toml::de::from_str::(file_content) + .context("Failed to parse the `info.toml` file")? + } Err(e) => { if e.kind() == ErrorKind::NotFound { return toml::de::from_str(EMBEDDED_FILES.info_file) From 17ff88902ba089285a8d635c4bda05dbaea94131 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 10 Sep 2025 18:19:41 +0200 Subject: [PATCH 19/78] Avoid initializing nested Git repository Previously a Git repository was initialized if a Cargo workspace was detected. However, it's also possible for users to initialize rustlings in an existing Git repository that doesn't contain a Cargo workspace. In that case, it's still undesirable to initialize a nested Git repository for rustlings. We therefore search all ancestors of the current working directory for `.git` or `.jj` directories to determine if rustlings is being initialized in an existing Git repository. --- src/init.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 971b0e51..2987907e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -29,6 +29,21 @@ pub fn init() -> Result<()> { bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } + let is_inside_vcs_repository = 'detect_repo: { + let Ok(mut dir) = std::env::current_dir() else { + break 'detect_repo false; + }; + loop { + if dir.join(".git").exists() || dir.join(".jj").exists() { + break 'detect_repo true; + } + match dir.parent() { + Some(parent) => dir = parent.into(), + None => break 'detect_repo false, + } + } + }; + let locate_project_output = Command::new("cargo") .arg("locate-project") .arg("-q") @@ -59,7 +74,7 @@ pub fn init() -> Result<()> { } let mut stdout = io::stdout().lock(); - let mut init_git = true; + let mut init_git = !is_inside_vcs_repository; if locate_project_output.status.success() { if Path::new("exercises").exists() && Path::new("solutions").exists() { From 064f057b102bd6bb0090d4ee3e307b40c28f4fa4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Feb 2026 18:01:17 +0100 Subject: [PATCH 20/78] Improve integration testing --- tests/integration_tests.rs | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 520429d0..c7119914 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -5,7 +5,7 @@ use std::{ }; enum Output<'a> { - FullStdout(&'a str), + FullStdout(&'a [u8]), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -61,32 +61,27 @@ impl<'a> Cmd<'a> { cmd.args(self.args).stdin(Stdio::null()); - let output = match self.output { - None => cmd.output().unwrap(), + let output = cmd.output().unwrap(); + match self.output { + None => (), Some(FullStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); - assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); - output + assert_eq!(output.stdout, stdout); } Some(PartialStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); - output } Some(PartialStderr(stderr)) => { - let output = cmd.stdout(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); - output } }; - assert_eq!( - output.status.success(), - success, - "{cmd:?}\n\nstdout:\n{}\nstderr:\n{}", - from_utf8(&output.stdout).unwrap(), - from_utf8(&output.stderr).unwrap(), - ); + if output.status.success() != success { + panic!( + "{cmd:?}\n\nstdout:\n{}\n\nstderr:\n{}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap(), + ); + } } #[inline] @@ -153,7 +148,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout("The answer to everything: 42\n")) + .output(FullStdout(b"The answer to everything: 42\n")) .success(); } From d87a3b6ca53016ed51da6ea94a1291a596a40db4 Mon Sep 17 00:00:00 2001 From: Gabriel Feceoru Date: Sat, 14 Mar 2026 16:26:22 +0100 Subject: [PATCH 21/78] Fix u16 mul overflow with big term width When running rustlings in Rover IDE, term width could have a value of 2480 which causes u16 mul overflow. --- src/term.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/term.rs b/src/term.rs index 3d149b33..b661dfa2 100644 --- a/src/term.rs +++ b/src/term.rs @@ -216,7 +216,9 @@ pub fn progress_bar<'a>( stdout.write_all(PREFIX)?; let width = term_width - WRAPPER_WIDTH; - let filled = (width * progress) / total; + // Use u32 to prevent the intermediate multiplication from overflowing u16 + let filled = (width as u32 * progress as u32) / total as u32; + let filled = filled as u16; stdout.queue(SetForegroundColor(Color::Green))?; for _ in 0..filled { From 611d62951f74040a750589bd42f84dfaccead8cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 16:57:50 +0100 Subject: [PATCH 22/78] Update deps --- Cargo.lock | 68 +++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c690e819..c0f0147a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,15 +19,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -78,9 +78,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -112,15 +112,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossterm" @@ -196,9 +196,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ "bitflags 2.11.0", "inotify-sys", @@ -306,9 +306,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "linux-raw-sys" @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -438,18 +438,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_syscall" @@ -627,9 +627,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap", "serde_core", @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "wit-bindgen" From 0ffeb1440294c0ec63b488fb7a1ac7ff1510c2fa Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:10:11 +0100 Subject: [PATCH 23/78] Avoid unneeded computation on full progress bar --- src/term.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/term.rs b/src/term.rs index b661dfa2..96b8745a 100644 --- a/src/term.rs +++ b/src/term.rs @@ -227,14 +227,13 @@ pub fn progress_bar<'a>( if filled < width { stdout.write_all(b">")?; - } - let width_minus_filled = width - filled; - if width_minus_filled > 1 { - let red_part_width = width_minus_filled - 1; - stdout.queue(SetForegroundColor(Color::Red))?; - for _ in 0..red_part_width { - stdout.write_all(b"-")?; + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + stdout.queue(SetForegroundColor(Color::Red))?; + for _ in 1..width_minus_filled { + stdout.write_all(b"-")?; + } } } From 337f6b152132438aa22c739aa79dcb17d9c9e239 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:18:39 +0100 Subject: [PATCH 24/78] Apply pedantic Clippy lints --- src/app_state.rs | 2 +- src/info_file.rs | 2 +- src/main.rs | 4 ++-- src/term.rs | 6 +++--- src/watch/state.rs | 2 +- src/watch/terminal_event.rs | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index a566e14b..ad63c6de 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -433,7 +433,7 @@ impl AppState { .is_err() { break; - }; + } let success = exercise.run_exercise(None, &slf.cmd_runner); let progress = match success { diff --git a/src/info_file.rs b/src/info_file.rs index 13206fd8..54a21a5c 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -23,7 +23,7 @@ pub struct ExerciseInfo { #[serde(default)] pub skip_check_unsolved: bool, } -#[inline(always)] +#[inline] const fn default_true() -> bool { true } diff --git a/src/main.rs b/src/main.rs index b541b68f..c39e8629 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,9 +171,9 @@ fn main() -> Result { stdout.write_all(b"\n")?; return Ok(ExitCode::FAILURE); - } else { - app_state.render_final_message(&mut stdout)?; } + + app_state.render_final_message(&mut stdout)?; } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; diff --git a/src/term.rs b/src/term.rs index 96b8745a..65e4c511 100644 --- a/src/term.rs +++ b/src/term.rs @@ -197,15 +197,15 @@ pub fn progress_bar<'a>( total: u16, term_width: u16, ) -> io::Result<()> { - debug_assert!(total <= 999); - debug_assert!(progress <= total); - const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + debug_assert!(total <= 999); + debug_assert!(progress <= total); + if term_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. diff --git a/src/watch/state.rs b/src/watch/state.rs index a92dd2d6..44cbd439 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -60,7 +60,7 @@ impl<'a> WatchState<'a> { watch_event_sender, terminal_event_unpause_receiver, manual_run, - ) + ); }) .context("Failed to spawn a thread to handle terminal events")?; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 2400a3df..439e4730 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -47,7 +47,7 @@ pub fn terminal_event_handler( // Pause input until quitting the confirmation prompt. if unpause_receiver.recv().is_err() { return; - }; + } continue; } @@ -64,7 +64,7 @@ pub fn terminal_event_handler( return; } } - Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue, + Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => (), Err(e) => break WatchEvent::TerminalEventErr(e), } }; From ceb98475e2a3a3c83df4c381a2da14afe3f8a2e8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 17:36:18 +0100 Subject: [PATCH 25/78] Avoid unneeded castings --- src/app_state.rs | 10 +++++----- src/list/state.rs | 2 +- src/term.rs | 11 +++++------ src/watch/state.rs | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index ad63c6de..5722e607 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -52,8 +52,8 @@ pub enum CheckProgress { pub struct AppState { current_exercise_ind: usize, exercises: Vec, - // Caches the number of done exercises to avoid iterating over all exercises every time. - n_done: u16, + // Cache the number of done exercises to avoid iterating over all exercises every time. + n_done: u32, final_message: &'static str, state_file: File, // Preallocated buffer for reading and writing the state file. @@ -191,13 +191,13 @@ impl AppState { } #[inline] - pub fn n_done(&self) -> u16 { + pub fn n_done(&self) -> u32 { self.n_done } #[inline] - pub fn n_pending(&self) -> u16 { - self.exercises.len() as u16 - self.n_done + pub fn n_pending(&self) -> u32 { + self.exercises.len() as u32 - self.n_done } #[inline] diff --git a/src/list/state.rs b/src/list/state.rs index 55ccb4c9..58aa4961 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -229,7 +229,7 @@ impl<'a> ListState<'a> { progress_bar( &mut MaxLenWriter::new(stdout, self.term_width as usize), self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; next_ln(stdout)?; diff --git a/src/term.rs b/src/term.rs index 65e4c511..8cab5005 100644 --- a/src/term.rs +++ b/src/term.rs @@ -193,8 +193,8 @@ impl Drop for ProgressCounter<'_, '_> { pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, - progress: u16, - total: u16, + progress: u32, + total: u32, term_width: u16, ) -> io::Result<()> { const PREFIX: &[u8] = b"Progress: ["; @@ -215,10 +215,9 @@ pub fn progress_bar<'a>( let stdout = writer.stdout(); stdout.write_all(PREFIX)?; - let width = term_width - WRAPPER_WIDTH; - // Use u32 to prevent the intermediate multiplication from overflowing u16 - let filled = (width as u32 * progress as u32) / total as u32; - let filled = filled as u16; + // Use u32 to prevent the intermediate multiplication from overflowing + let width = u32::from(term_width - WRAPPER_WIDTH); + let filled = (width * progress) / total; stdout.queue(SetForegroundColor(Color::Green))?; for _ in 0..filled { diff --git a/src/watch/state.rs b/src/watch/state.rs index 44cbd439..f93ea0cf 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -245,7 +245,7 @@ impl<'a> WatchState<'a> { progress_bar( stdout, self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; From a28b9eda84db5d1810f77225aee9d95ae960da2b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 14 Mar 2026 18:22:27 +0100 Subject: [PATCH 26/78] Delay inside_vcs_repo check until Git initialization --- src/init.rs | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/init.rs b/src/init.rs index 2987907e..f043bd48 100644 --- a/src/init.rs +++ b/src/init.rs @@ -5,7 +5,7 @@ use crossterm::{ }; use serde::Deserialize; use std::{ - env::set_current_dir, + env::{current_dir, set_current_dir}, fs::{self, create_dir}, io::{self, Write}, path::Path, @@ -29,21 +29,6 @@ pub fn init() -> Result<()> { bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } - let is_inside_vcs_repository = 'detect_repo: { - let Ok(mut dir) = std::env::current_dir() else { - break 'detect_repo false; - }; - loop { - if dir.join(".git").exists() || dir.join(".jj").exists() { - break 'detect_repo true; - } - match dir.parent() { - Some(parent) => dir = parent.into(), - None => break 'detect_repo false, - } - } - }; - let locate_project_output = Command::new("cargo") .arg("locate-project") .arg("-q") @@ -74,7 +59,7 @@ pub fn init() -> Result<()> { } let mut stdout = io::stdout().lock(); - let mut init_git = !is_inside_vcs_repository; + let mut init_git = true; if locate_project_output.status.success() { if Path::new("exercises").exists() && Path::new("solutions").exists() { @@ -184,14 +169,27 @@ pub fn init() -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - if init_git { - // Ignore any Git error because Git initialization is not required. - let _ = Command::new("git") - .arg("init") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + if init_git && let Ok(dir) = current_dir() { + let mut dir = dir.as_path(); + + loop { + if dir.join(".git").exists() || dir.join(".jj").exists() { + break; + } + + if let Some(parent) = dir.parent() { + dir = parent; + } else { + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + break; + } + } } stdout.queue(SetForegroundColor(Color::Green))?; From 2c9c31e8a2845f83bb41fe5af385b013473c4867 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 12:05:38 +0100 Subject: [PATCH 27/78] Remove cargo-upgrades from release hook --- release-hook.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/release-hook.sh b/release-hook.sh index 49349337..0e955d1f 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -4,7 +4,6 @@ set -e typos -cargo upgrades # Similar to CI cargo clippy -- --deny warnings From d3df1051675fc45cbfca6d8bc8a07f7c4e63cbf1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 12:17:07 +0100 Subject: [PATCH 28/78] Upgrade to Zola 0.22 --- .github/workflows/website.yml | 4 ++-- website/config.toml | 6 +++--- website/input.css | 8 ++------ website/package.json | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index d7513581..399d4674 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -19,11 +19,11 @@ jobs: - name: Build CSS run: npx @tailwindcss/cli -m -i input.css -o static/main.css - name: Download Zola - run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.20.0/zola-v0.20.0-x86_64-unknown-linux-gnu.tar.gz | tar xz + run: curl -fsSL https://github.com/getzola/zola/releases/download/v0.22.1/zola-v0.22.1-x86_64-unknown-linux-gnu.tar.gz | tar xz - name: Build site run: ./zola build - name: Upload static files as artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: website/public/ diff --git a/website/config.toml b/website/config.toml index 0c01dc7d..73ae7f2c 100644 --- a/website/config.toml +++ b/website/config.toml @@ -6,11 +6,11 @@ compile_sass = false build_search_index = false [markdown] -highlight_code = true -highlight_theme = "dracula" - insert_anchor_links = "heading" +[markdown.highlighting] +theme = "dracula" + [extra] logo_path = "images/happy_ferris.svg" diff --git a/website/input.css b/website/input.css index af0675d8..01c956d6 100644 --- a/website/input.css +++ b/website/input.css @@ -41,14 +41,10 @@ @apply md:w-3/4 lg:w-3/5; } blockquote { - @apply px-3 pt-2 pb-0.5 mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm; + @apply px-3 pt-2 pb-px mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm; } pre { - @apply px-2 pt-2 pb-px overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 after:content-[attr(data-lang)] after:text-[8px] after:opacity-40 selection:bg-white/15; - } - pre code mark { - @apply pb-0.5 pt-1 pr-px text-inherit rounded-xs; + @apply px-2 pt-2 pb-1.5 overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 selection:bg-white/15; } } - diff --git a/website/package.json b/website/package.json index 38dd27e9..80c84872 100644 --- a/website/package.json +++ b/website/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@tailwindcss/cli": "^4.1" + "@tailwindcss/cli": "^4" } } From 573d5a2acd5a6541c459b228d994e5281b764684 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 17 Mar 2026 11:56:36 +0100 Subject: [PATCH 29/78] Fix integration tests for build dir layout v2 --- tests/integration_tests.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index c7119914..91d0536e 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,4 @@ use std::{ - env::{self, consts::EXE_SUFFIX}, process::{Command, Stdio}, str::from_utf8, }; @@ -40,20 +39,7 @@ impl<'a> Cmd<'a> { #[track_caller] fn assert(&self, success: bool) { - let rustlings_bin = { - let mut path = env::current_exe().unwrap(); - // Pop test binary name - path.pop(); - // Pop `/deps` - path.pop(); - - path.push("rustlings"); - let mut path = path.into_os_string(); - path.push(EXE_SUFFIX); - path - }; - - let mut cmd = Command::new(rustlings_bin); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_rustlings")); if let Some(current_dir) = self.current_dir { cmd.current_dir(current_dir); From 08eb634db5f76c891a3f0f7e916d4cc41ec03da4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:24:41 +0100 Subject: [PATCH 30/78] Upgrade deply-pages --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 399d4674..79d70f74 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -40,4 +40,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 From 3e46d8c50a23ae2ceea7f9abb350c3049ff79fb3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:28:39 +0100 Subject: [PATCH 31/78] Update deps --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0f0147a..a65ecff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,9 +274,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kqueue" @@ -564,9 +564,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" dependencies = [ "indexmap", "serde_core", @@ -655,27 +655,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "unicode-ident" @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" From 37cbcd9049627653a46f3a90d0d5dd5468464e1c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 25 Mar 2026 18:28:39 +0100 Subject: [PATCH 32/78] Adjust CI triggers --- .github/workflows/rust.yml | 2 ++ .github/workflows/website.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9a7ae2c0..72f3675b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,11 +5,13 @@ on: branches: [main] paths-ignore: - website + - .github/workflows/website.yml - '*.md' pull_request: branches: [main] paths-ignore: - website + - .github/workflows/website.yml - '*.md' env: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 79d70f74..7ea41e73 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -4,7 +4,9 @@ on: workflow_dispatch: push: branches: [main] - paths: [website] + paths: + - website + - .github/workflows/website.yml jobs: build: From 7150a9eb79842f68e95ec045cf21ffbc0bb07915 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 30 Mar 2026 16:55:07 +0200 Subject: [PATCH 33/78] Add rumdl --- .github/workflows/rust.yml | 5 + .github/workflows/website.yml | 7 +- .rumdl.toml | 7 ++ CHANGELOG.md | 192 +++++++++++++++++----------------- CONTRIBUTING.md | 2 +- 5 files changed, 116 insertions(+), 97 deletions(-) create mode 100644 .rumdl.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 72f3675b..a52d0730 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,3 +49,8 @@ jobs: - uses: swatinem/rust-cache@v2 - name: rustlings dev check run: cargo dev check --require-solutions + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7ea41e73..d437cf03 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -9,7 +9,13 @@ on: - .github/workflows/website.yml jobs: + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 build: + needs: rumdl defaults: run: working-directory: website @@ -28,7 +34,6 @@ jobs: uses: actions/upload-pages-artifact@v4 with: path: website/public/ - deploy: needs: build # Grant GITHUB_TOKEN the permissions required to make a Pages deployment diff --git a/.rumdl.toml b/.rumdl.toml new file mode 100644 index 00000000..528e0e31 --- /dev/null +++ b/.rumdl.toml @@ -0,0 +1,7 @@ +[global] +output-format = "full" +disable = ["MD013", "MD057"] + +[per-file-ignores] +"website/content/_index.md" = ["MD041"] +"website/content/**/*.md" = ["MD028", "MD033"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2b4153..0f326b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## Unreleased ### Changed @@ -109,17 +111,17 @@ ## 6.1.0 (2024-07-10) -#### Added +### Added - `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment. - `dev check`: Check that all exercises actually fail to run (not already solved). -#### Changed +### Changed - Make enum variants more consistent between enum exercises. - `iterators3`: Teach about the possible case of integer overflow during division. -#### Fixed +### Fixed - Exit with a helpful error message on missing/unsupported terminal/TTY. - Mark the last exercise as done. @@ -196,11 +198,11 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.1 (2023-09-18) -#### Changed +### Changed - Converted all exercises with assertions to test mode. -#### Fixed +### Fixed - `cow1`: Reverted regression introduced by calling `to_mut` where it shouldn't have been called, and clarified comment. @@ -211,7 +213,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.0 (2023-09-04) -#### Added +### Added - New exercise: `if3`, teaching the user about `if let` statements. - `hashmaps2`: Added an extra test function to check if the amount of fruits is higher than zero. @@ -219,7 +221,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `if1`: Added a test case to check equal values. - `if3`: Added a note specifying that there are no test changes needed. -#### Changed +### Changed - Swapped the order of threads and smart pointer exercises. - Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :) @@ -227,7 +229,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same concepts. -#### Fixed +### Fixed - `iterators5`: - Removed an outdated part of the hint. @@ -242,7 +244,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `cow1`: Added `.to_mut()` to distinguish from the previous test case. - `threads2`: Updated hint text to reference the correct book heading. -#### Housekeeping +### Housekeeping - Cleaned up the explanation paragraphs at the start of each exercise. - Lots of Nix housekeeping that I don't feel qualified to write about! @@ -250,13 +252,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.5.1 (2023-05-17) -#### Fixed +### Fixed - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. ## 5.5.0 (2023-05-17) -#### Added +### Added - `strings2`: Added a reference to the book chapter for reference conversion - `lifetimes`: Added a link to the lifetimekata project @@ -264,7 +266,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added a `!` prefix command to watch mode that runs an external command - Added a `--success-hints` option to watch mode that shows hints on exercise success -#### Changed +### Changed - `vecs2`: Renamed iterator variable bindings for clarify - `lifetimes`: Changed order of book references @@ -273,7 +275,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `options2`: Improved tests for layering options - `modules2`: Added more information to the hint -#### Fixed +### Fixed - `errors2`: Corrected a comment wording - `iterators2`: Fixed a spelling mistake in the hint text @@ -283,20 +285,20 @@ Then follow the link to the guide about [community exercises](https://rustlings. - `options3`: Changed exercise to panic when no match is found - `rustlings lsp` now generates absolute paths, which should fix VSCode `rust-analyzer` usage on Windows -#### Housekeeping +### Housekeeping - Added a markdown linter to run on GitHub actions - Split quick installation section into two code blocks ## 5.4.1 (2023-03-10) -#### Changed +### Changed - `vecs`: Added links to `iter_mut` and `map` to README.md - `cow1`: Changed main to tests - `iterators1`: Formatted according to rustfmt -#### Fixed +### Fixed - `errors5`: Unified undisclosed type notation - `arc1`: Improved readability by avoiding implicit dereference @@ -305,7 +307,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.4.0 (2023-02-12) -#### Changed +### Changed - Reordered exercises - Unwrapped `standard_library_types` into `iterators` and `smart_pointers` @@ -317,7 +319,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Made progress bar update proportional to amount of files verified - Decreased `watch` delay from 2 to 1 second -#### Fixed +### Fixed - Capitalized "Rust" in exercise hints - **enums3**: Removed superfluous tuple brackets @@ -327,23 +329,23 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fixed a typo in a method name - Specified the edition in `rustc` commands -#### Housekeeping +### Housekeeping - Bumped min Rust version to 1.58 in installation script ## 5.3.0 (2022-12-23) -#### Added +### Added - **cli**: Added a percentage display in watch mode - Added a `flake.nix` for Nix users -#### Changed +### Changed - **structs3**: Added an additional test - **macros**: Added a link to MacroKata in the README -#### Fixed +### Fixed - **strings3**: Added a link to `std` in the hint - **threads1**: Corrected a hint link @@ -357,7 +359,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **enums2**: Removed unnecessary indirection of self - **enums3**: Added an extra tuple comment -#### Housekeeping +### Housekeeping - Added a VSCode extension recommendation - Applied some Clippy and rustfmt formatting @@ -365,28 +367,28 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.2.1 (2022-09-06) -#### Fixed +### Fixed - **quiz1**: Reworded the comment to actually reflect what's going on in the tests. Also added another assert just to make sure. - **rc1**: Fixed a typo in the hint. - **lifetimes**: Add quotes to the `println!` output, for readability. -#### Housekeeping +### Housekeeping - Fixed a typo in README.md ## 5.2.0 (2022-08-27) -#### Added +### Added - Added a `reset` command -#### Changed +### Changed - **options2**: Convert the exercise to use tests -#### Fixed +### Fixed - **threads3**: Fixed a typo - **quiz1**: Adjusted the explanations to be consistent with @@ -394,18 +396,18 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.1.1 (2022-08-17) -#### Bug Fixes +### Bug Fixes - Fixed an incorrect assertion in options1 ## 5.1.0 (2022-08-16) -#### Features +### Features - Added a new `rc1` exercise. - Added a new `cow1` exercise. -#### Bug Fixes +### Bug Fixes - **variables5**: Corrected reference to previous exercise - **functions4**: Fixed line number reference @@ -425,7 +427,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added more granular tests - Fixed some comment syntax shenanigans in info.toml -#### Housekeeping +### Housekeeping - Fixed a typo in .editorconfig - Fixed a typo in integration_tests.rs @@ -434,7 +436,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.0.0 (2022-07-16) -#### Features +### Features - Hint comments in exercises now also include a reference to the `hint` watch mode subcommand. @@ -466,7 +468,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added 3 new lifetimes exercises. - Added 3 new traits exercises. -#### Bug Fixes +### Bug Fixes - **variables2**: Made output messages more verbose. - **variables5**: Added a nudging hint about shadowing. @@ -490,7 +492,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. `Box`. - **try_from_into**: Fixed the function name in comment. -#### Removed +### Removed - Removed the legacy LSP feature that was using `mod.rs` files. - Removed `quiz4`. @@ -498,7 +500,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. order, and I've always felt like they didn't quite fit in with the mostly simple, book-following style we've had in Rustlings. -#### Housekeeping +### Housekeeping - Added missing exercises to the book index. - Updated spacing in Cargo.toml. @@ -506,53 +508,53 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.8.0 (2022-07-01) -#### Features +### Features - Added a progress indicator for `rustlings watch`. - The installation script now checks for Rustup being installed. - Added a `rustlings lsp` command to enable `rust-analyzer`. -#### Bug Fixes +### Bug Fixes - **move_semantics5**: Replaced "in vogue" with "in scope" in hint. - **if2**: Fixed a typo in the hint. - **variables1**: Fixed an incorrect line reference in the hint. - Fixed an out of bounds check in the installation Bash script. -#### Housekeeping +### Housekeeping - Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting. - Removed the deprecated Rust GitPod extension. ## 4.7.1 (2022-04-20) -#### Features +### Features - The amount of dependency crates that need to be compiled went down from ~65 to ~45 by bumping dependency versions. - The minimum Rust version in the install scripts has been bumped to 1.56.0 (this isn't in the release itself, since install scripts don't really get versioned) -#### Bug Fixes +### Bug Fixes - **arc1**: A small part has been rewritten using a more functional code style (#968). - **using_as**: A small part has been refactored to use `sum` instead of `fold`, resulting in better readability. -#### Housekeeping +### Housekeeping - The changelog will now be manually written instead of being automatically generated by the Git log. ## 4.7.0 (2022-04-14) -#### Features +### Features - Add move_semantics6.rs exercise (#908) ([3f0e1303](https://github.com/rust-lang/rustlings/commit/3f0e1303e0b3bf3fecc0baced3c8b8a37f83c184)) - **intro:** Add intro section. ([21c9f441](https://github.com/rust-lang/rustlings/commit/21c9f44168394e08338fd470b5f49b1fd235986f)) - Include exercises folder in the project structure behind a feature, enabling rust-analyzer to work (#917) ([179a75a6](https://github.com/rust-lang/rustlings/commit/179a75a68d03ac9518dec2297fb17f91a4fc506b)) -#### Bug Fixes +### Bug Fixes - Fix a few spelling mistakes ([1c0fe3cb](https://github.com/rust-lang/rustlings/commit/1c0fe3cbcca85f90b3985985b8e265ee872a2ab2)) - **cli:** @@ -579,14 +581,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **structs3.rs:** assigned value to cents_per_gram in test ([d1ee2daf](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) - **traits1:** rename test functions to snake case (#854) ([1663a16e](https://github.com/rust-lang/rustlings/commit/1663a16eade6ca646b6ed061735f7982434d530d)) -#### Documentation improvements +### Documentation improvements - Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886)) - Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929)) ## 4.6.0 (2021-09-25) -#### Features +### Features - add advanced_errs2 ([abd6b70c](https://github.com/rust-lang/rustlings/commit/abd6b70c72dc6426752ff41f09160b839e5c449e)) - add advanced_errs1 ([882d535b](https://github.com/rust-lang/rustlings/commit/882d535ba8628d5e0b37e8664b3e2f26260b2671)) @@ -595,7 +597,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **modules:** update exercises, add modules3 (#822) ([dfd2fab4](https://github.com/rust-lang/rustlings/commit/dfd2fab4f33d1bf59e2e5ee03123c0c9a67a9481)) - **quiz1:** add default function name in comment (#838) ([0a11bad7](https://github.com/rust-lang/rustlings/commit/0a11bad71402b5403143d642f439f57931278c07)) -#### Bug Fixes +### Bug Fixes - Correct small typo in exercises/conversions/from_str.rs ([86cc8529](https://github.com/rust-lang/rustlings/commit/86cc85295ae36948963ae52882e285d7e3e29323)) - **cli:** typo in exercise.rs (#848) ([06d5c097](https://github.com/rust-lang/rustlings/commit/06d5c0973a3dffa3c6c6f70acb775d4c6630323c)) @@ -608,12 +610,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.5.0 (2021-07-07) -#### Features +### Features - Add move_semantics5 exercise. (#746) ([399ab328](https://github.com/rust-lang/rustlings/commit/399ab328d8d407265c09563aa4ef4534b2503ff2)) - **cli:** Add "next" to run the next unsolved exercise. (#785) ([d20e413a](https://github.com/rust-lang/rustlings/commit/d20e413a68772cd493561f2651cf244e822b7ca5)) -#### Bug Fixes +### Bug Fixes - rename result1 to errors4 ([50ab289d](https://github.com/rust-lang/rustlings/commit/50ab289da6b9eb19a7486c341b00048c516b88c0)) - move_semantics5 hints ([1b858285](https://github.com/rust-lang/rustlings/commit/1b85828548f46f58b622b5e0c00f8c989f928807)) @@ -628,7 +630,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.4.0 (2021-04-24) -#### Bug Fixes +### Bug Fixes - Fix spelling error in main.rs ([91ee27f2](https://github.com/rust-lang/rustlings/commit/91ee27f22bd3797a9db57e5fd430801c170c5db8)) - typo in default out text ([644c49f1](https://github.com/rust-lang/rustlings/commit/644c49f1e04cbb24e95872b3a52b07d692ae3bc8)) @@ -656,7 +658,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **threads1:** line number correction ([7857b0a6](https://github.com/rust-lang/rustlings/commit/7857b0a689b0847f48d8c14cbd1865e3b812d5ca)) - **try_from_into:** use trait objects ([2e93a588](https://github.com/rust-lang/rustlings/commit/2e93a588e0abe8badb7eafafb9e7d073c2be5df8)) -#### Features +### Features - Replace clap with argh ([7928122f](https://github.com/rust-lang/rustlings/commit/7928122fcef9ca7834d988b1ec8ca0687478beeb)) - Replace emojis when NO_EMOJI env variable present ([8d62a996](https://github.com/rust-lang/rustlings/commit/8d62a9963708dbecd9312e8bcc4b47049c72d155)) @@ -669,7 +671,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.3.0 (2020-12-29) -#### Features +### Features - Rewrite default out text ([44d39112](https://github.com/rust-lang/rustlings/commit/44d39112ff122b29c9793fe52e605df1612c6490)) - match exercise order to book chapters (#541) ([033bf119](https://github.com/rust-lang/rustlings/commit/033bf1198fc8bfce1b570e49da7cde010aa552e3)) @@ -677,7 +679,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - add "rustlings list" command ([838f9f30](https://github.com/rust-lang/rustlings/commit/838f9f30083d0b23fd67503dcf0fbeca498e6647)) - **try_from_into:** remove duplicate annotation ([04f1d079](https://github.com/rust-lang/rustlings/commit/04f1d079aa42a2f49af694bc92c67d731d31a53f)) -#### Bug Fixes +### Bug Fixes - update structs README ([bcf14cf6](https://github.com/rust-lang/rustlings/commit/bcf14cf677adb3a38a3ac3ca53f3c69f61153025)) - added missing exercises to info.toml ([90cfb6ff](https://github.com/rust-lang/rustlings/commit/90cfb6ff28377531bfc34acb70547bdb13374f6b)) @@ -691,14 +693,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.2.0 (2020-11-07) -#### Features +### Features - Add HashMap exercises ([633c00cf](https://github.com/rust-lang/rustlings/commit/633c00cf8071e1e82959a3010452a32f34f29fc9)) - Add Vec exercises ([0c12fa31](https://github.com/rust-lang/rustlings/commit/0c12fa31c57c03c6287458a0a8aca7afd057baf6)) - **primitive_types6:** Add a test (#548) ([2b1fb2b7](https://github.com/rust-lang/rustlings/commit/2b1fb2b739bf9ad8d6b7b12af25fee173011bfc4)) - **try_from_into:** Add tests (#571) ([95ccd926](https://github.com/rust-lang/rustlings/commit/95ccd92616ae79ba287cce221101e0bbe4f68cdc)) -#### Bug Fixes +### Bug Fixes - log error output when inotify limit is exceeded ([d61b4e5a](https://github.com/rust-lang/rustlings/commit/d61b4e5a13b44d72d004082f523fa1b6b24c1aca)) - more unique temp_file ([5643ef05](https://github.com/rust-lang/rustlings/commit/5643ef05bc81e4a840e9456f4406a769abbe1392)) @@ -711,7 +713,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.1.0 (2020-10-05) -#### Bug Fixes +### Bug Fixes - Update rustlings version in Cargo.lock ([1cc40bc9](https://github.com/rust-lang/rustlings/commit/1cc40bc9ce95c23d56f6d91fa1c4deb646231fef)) - **arc1:** index mod should equal thread count ([b4062ef6](https://github.com/rust-lang/rustlings/commit/b4062ef6993e80dac107c4093ea85166ad3ee0fa)) @@ -721,7 +723,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **structs3:** Small adjustment of variable name ([114b54cb](https://github.com/rust-lang/rustlings/commit/114b54cbdb977234b39e5f180d937c14c78bb8b2)) - **using_as:** Add test so that proper type is returned. (#512) ([3286c5ec](https://github.com/rust-lang/rustlings/commit/3286c5ec19ea5fb7ded81d047da5f8594108a490)) -#### Features +### Features - Added iterators1.rs exercise ([9642f5a3](https://github.com/rust-lang/rustlings/commit/9642f5a3f686270a4f8f6ba969919ddbbc4f7fdd)) - Add ability to run rustlings on repl.it (#471) ([8f7b5bd0](https://github.com/rust-lang/rustlings/commit/8f7b5bd00eb83542b959830ef55192d2d76db90a)) @@ -733,12 +735,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.0.0 (2020-07-08) -#### Breaking Changes +### Breaking Changes - Add a --nocapture option to display test harnesses' outputs ([8ad5f9bf](https://github.com/rust-lang/rustlings/commit/8ad5f9bf531a4848b1104b7b389a20171624c82f)) - Rename test to quiz, fixes #244 ([010a0456](https://github.com/rust-lang/rustlings/commit/010a04569282149cea7f7a76fc4d7f4c9f0f08dd)) -#### Features +### Features - Add traits README ([173bb141](https://github.com/rust-lang/rustlings/commit/173bb14140c5530cbdb59e53ace3991a99d804af)) - Add box1.rs exercise ([7479a473](https://github.com/rust-lang/rustlings/commit/7479a4737bdcac347322ad0883ca528c8675e720)) @@ -747,7 +749,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Added exercise structs3.rs ([b66e2e09](https://github.com/rust-lang/rustlings/commit/b66e2e09622243e086a0f1258dd27e1a2d61c891)) - Add exercise variables6 covering const (#352) ([5999acd2](https://github.com/rust-lang/rustlings/commit/5999acd24a4f203292be36e0fd18d385887ec481)) -#### Bug Fixes +### Bug Fixes - Change then to than ([ddd98ad7](https://github.com/rust-lang/rustlings/commit/ddd98ad75d3668fbb10eff74374148aa5ed2344d)) - rename quiz1 to tests1 in info (#420) ([0dd1c6ca](https://github.com/rust-lang/rustlings/commit/0dd1c6ca6b389789e0972aa955fe17aa15c95f29)) @@ -774,11 +776,11 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 3.0.0 (2020-04-11) -#### Breaking Changes +### Breaking Changes - make "compile" exercises print output (#278) ([3b6d5c](https://github.com/fmoko/rustlings/commit/3b6d5c3aaa27a242a832799eb66e96897d26fde3)) -#### Bug Fixes +### Bug Fixes - **primitive_types:** revert primitive_types4 (#296) ([b3a3351e](https://github.com/rust-lang/rustlings/commit/b3a3351e8e6a0bdee07077d7b0382953821649ae)) - **run:** compile clippy exercise files (#295) ([3ab084a4](https://github.com/rust-lang/rustlings/commit/3ab084a421c0f140ae83bf1fc3f47b39342e7373)) @@ -787,26 +789,26 @@ Then follow the link to the guide about [community exercises](https://rustlings. - remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342)) - don't hardcode documentation version for traits (#288) ([30e6af](https://github.com/fmoko/rustlings/commit/30e6af60690c326fb5d3a9b7335f35c69c09137d)) -#### Features +### Features - add Option2 exercise (#290) ([86b5c08b](https://github.com/rust-lang/rustlings/commit/86b5c08b9bea1576127a7c5f599f5752072c087d)) - add exercise for option (#282) ([135e5d47](https://github.com/rust-lang/rustlings/commit/135e5d47a7c395aece6f6022117fb20c82f2d3d4)) - add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229)) - **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71)) -### 2.2.1 (2020-02-27) +## 2.2.1 (2020-02-27) -#### Bug Fixes +### Bug Fixes - Re-add cloning the repo to install scripts ([3d9b03c5](https://github.com/rust-lang/rustlings/commit/3d9b03c52b8dc51b140757f6fd25ad87b5782ef5)) -#### Features +### Features - Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921)) ## 2.2.0 (2020-02-25) -#### Bug Fixes +### Bug Fixes - Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401)) - **docs:** @@ -821,7 +823,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce) - Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf)) -#### Features +### Features - Add variables5.rs exercise (#264) ([0c73609e](https://github.com/rust-lang/rustlings/commit/0c73609e6f2311295e95d6f96f8c747cfc4cba03)) - Show a completion message when watching (#253) ([d25ee55a](https://github.com/rust-lang/rustlings/commit/d25ee55a3205882d35782e370af855051b39c58c)) @@ -833,7 +835,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 2.1.0 (2019-11-27) -#### Bug Fixes +### Bug Fixes - add line numbers in several exercises and hints ([b565c4d3](https://github.com/rust-lang/rustlings/commit/b565c4d3e74e8e110bef201a082fa1302722a7c3)) - **arc1:** Fix some words in the comment ([c42c3b21](https://github.com/rust-lang/rustlings/commit/c42c3b2101df9164c8cd7bb344def921e5ba3e61)) @@ -844,33 +846,33 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **strings2:** update line number in hint ([a09f684f](https://github.com/rust-lang/rustlings/commit/a09f684f05c58d239a6fc59ec5f81c2533e8b820)) - **variables1:** Correct wrong word in comment ([fda5a470](https://github.com/rust-lang/rustlings/commit/fda5a47069e0954f16a04e8e50945e03becb71a5)) -#### Features +### Features - **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c)) ## 2.0.0 (2019-11-12) -#### Bug Fixes +### Bug Fixes - **default:** Clarify the installation procedure ([c371b853](https://github.com/rust-lang/rustlings/commit/c371b853afa08947ddeebec0edd074b171eeaae0)) - **info:** Fix trailing newlines for hints ([795b6e34](https://github.com/rust-lang/rustlings/commit/795b6e348094a898e9227a14f6232f7bb94c8d31)) - **run:** make `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Breaking Changes +### Breaking Changes - Refactor hint system ([9bdb0a12](https://github.com/rust-lang/rustlings/commit/9bdb0a12e45a8e9f9f6a4bd4a9c172c5376c7f60)) - improve `watch` execution mode ([2cdd6129](https://github.com/rust-lang/rustlings/commit/2cdd61294f0d9a53775ee24ad76435bec8a21e60)) - Index exercises by name ([627cdc07](https://github.com/rust-lang/rustlings/commit/627cdc07d07dfe6a740e885e0ddf6900e7ec336b)) - **run:** makes `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Features +### Features - **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425)) - **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f)) -### 1.5.1 (2019-11-11) +## 1.5.1 (2019-11-11) -#### Bug Fixes +### Bug Fixes - **errors3:** Update hint ([dcfb427b](https://github.com/rust-lang/rustlings/commit/dcfb427b09585f0193f0a294443fdf99f11c64cb), closes [#185](https://github.com/rust-lang/rustlings/issues/185)) - **if1:** Remove `return` reference ([ad03d180](https://github.com/rust-lang/rustlings/commit/ad03d180c9311c0093e56a3531eec1a9a70cdb45)) @@ -881,7 +883,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.5.0 (2019-11-09) -#### Bug Fixes +### Bug Fixes - **test1:** Rewrite logic ([79a56942](https://github.com/rust-lang/rustlings/commit/79a569422c8309cfc9e4aed25bf4ab3b3859996b)) - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) @@ -897,15 +899,15 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b)) -#### Features +### Features - Add enums exercises ([dc150321](https://github.com/rust-lang/rustlings/commit/dc15032112fc485226a573a18139e5ce928b1755)) - Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) -### 1.4.1 (2019-08-13) +## 1.4.1 (2019-08-13) -#### Bug Fixes +### Bug Fixes - **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac)) - **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) @@ -913,7 +915,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.4.0 (2019-07-13) -#### Bug Fixes +### Bug Fixes - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) - **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6)) @@ -922,18 +924,18 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **cli:** Check if changed exercise file exists before calling verify ([ba85ca3](https://github.com/rust-lang/rustlings/commit/ba85ca32c4cfc61de46851ab89f9c58a28f33c88)) - **structs1:** Fix the irrefutable let pattern warning ([cc6a141](https://github.com/rust-lang/rustlings/commit/cc6a14104d7c034eadc98297eaaa972d09c50b1f)) -#### Features +### Features - **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) -### 1.3.0 (2019-06-05) +## 1.3.0 (2019-06-05) -#### Features +### Features - Adds a simple exercise for structures (#163, @briankung) -#### Bug Fixes +### Bug Fixes - Add Result type signature as it is difficult for new comers to understand Generics and Error all at once. (#157, @veggiemonk) - Rustfmt and whitespace fixes (#161, @eddyp) @@ -942,29 +944,29 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fix broken link (#164, @HanKruiger) - Remove highlighting and syntect (#167, @komaeda) -### 1.2.2 (2019-05-07) +## 1.2.2 (2019-05-07) -#### Bug Fixes +### Bug Fixes - Reverted `--nocapture` flag since it was causing tests to pass unconditionally -### 1.2.1 (2019-04-22) +## 1.2.1 (2019-04-22) -#### Bug Fixes +### Bug Fixes - Fix the `--nocapture` feature (@komaeda) - Provide a nicer error message for when you're in the wrong directory -### 1.2.0 (2019-04-22) +## 1.2.0 (2019-04-22) -#### Features +### Features - Add errors to exercises that compile without user changes (@yvan-sraka) - Use --nocapture when testing, enabling `println!` when running (@komaeda) -### 1.1.1 (2019-04-14) +## 1.1.1 (2019-04-14) -#### Bug fixes +### Bug fixes - Fix permissions on exercise files (@zacanger, #133) - Make installation checks more thorough (@komaeda, 1b3469f236bc6979c27f6e1a04e4138a88e55de3) @@ -974,7 +976,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fix links by deleting book version (@diodfr, #142) - Canonicalize paths to fix path matching (@cjpearce, #143) -### 1.1.0 (2019-03-20) +## 1.1.0 (2019-03-20) - errors2.rs: update link to Rust book (#124) - Start verification at most recently modified file (#120) @@ -983,12 +985,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Give a warning when Rustlings isn't run from the right directory (#123) - Verify that rust version is recent enough to install Rustlings (#131) -### 1.0.1 (2019-03-06) +## 1.0.1 (2019-03-06) - Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`) - Makes `rustlings watch` react to create file events (@shaunbennett, #117) - Reworks the exercise management to use an external TOML file instead of just listing them in the code -### 1.0.0 (2019-03-06) +## 1.0.0 (2019-03-06) Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95605f70..7b684d39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ I want to … ## Issues -You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). +You can [open an issue](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: - `cargo --version` From 7d53dc4c95d6b27761f157f00b958c120104f066 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 30 Mar 2026 17:35:45 +0200 Subject: [PATCH 34/78] Update deps --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a65ecff7..4c2d6d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" [[package]] name = "foldhash" @@ -236,9 +236,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -306,9 +306,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "linux-raw-sys" @@ -345,9 +345,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -515,9 +515,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -564,9 +564,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -655,27 +655,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "unicode-ident" @@ -885,9 +885,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" [[package]] name = "wit-bindgen" From 7ed231604099c347823c53c3874a8bdba59ca87a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 15:45:31 +0200 Subject: [PATCH 35/78] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326b60..8b9b356b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,14 @@ ## Unreleased +### Fixed + +- Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) +- Fix workspace detection on Windows [@senekor](https://github.com/senekor) + ### Changed +- Avoid initializing a nested Git repository [@senekor](https://github.com/senekor) - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. ## 6.5.0 (2025-08-21) From c466d01da938029e12d14514e47deb17c4b4e726 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:19:11 +0200 Subject: [PATCH 36/78] Unify imports --- src/cli.rs | 41 +++++++++++++++++++++++++ src/dev.rs | 4 +-- src/list.rs | 7 +++-- src/list/state.rs | 3 +- src/main.rs | 61 +++++++++---------------------------- src/watch.rs | 3 +- src/watch/notify_event.rs | 2 +- src/watch/state.rs | 3 +- src/watch/terminal_event.rs | 2 +- 9 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..2bea5544 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,41 @@ +use clap::{Parser, Subcommand}; + +use crate::dev::DevCommand; + +/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code +#[derive(Parser)] +#[command(version)] +pub struct Args { + #[command(subcommand)] + pub command: Option, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + pub manual_run: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + Run { + /// The name of the exercise + name: Option, + }, + /// Check all the exercises, marking them as done or pending accordingly. + CheckAll, + /// Reset a single exercise + Reset { + /// The name of the exercise + name: String, + }, + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + Hint { + /// The name of the exercise + name: Option, + }, + /// Commands for developing (community) Rustlings exercises + #[command(subcommand)] + Dev(DevCommand), +} diff --git a/src/dev.rs b/src/dev.rs index 41fddbeb..f2be6066 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -7,7 +7,7 @@ mod new; mod update; #[derive(Subcommand)] -pub enum DevCommands { +pub enum DevCommand { /// Create a new project for community exercises New { /// The path to create the project in @@ -26,7 +26,7 @@ pub enum DevCommands { Update, } -impl DevCommands { +impl DevCommand { pub fn run(self) -> Result<()> { match self { Self::New { path, no_git } => { diff --git a/src/list.rs b/src/list.rs index a2eee9e1..c60a5299 100644 --- a/src/list.rs +++ b/src/list.rs @@ -11,9 +11,10 @@ use crossterm::{ }; use std::io::{self, StdoutLock, Write}; -use crate::app_state::AppState; - -use self::state::{Filter, ListState}; +use crate::{ + app_state::AppState, + list::state::{Filter, ListState}, +}; mod scroll_state; mod state; diff --git a/src/list/state.rs b/src/list/state.rs index 58aa4961..4e097613 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -15,11 +15,10 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, + list::scroll_state::ScrollState, term::{CountedWrite, MaxLenWriter, progress_bar}, }; -use super::scroll_state::ScrollState; - const COL_SPACING: usize = 2; const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Reverse) diff --git a/src/main.rs b/src/main.rs index c39e8629..8f31703b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; -use clap::{Parser, Subcommand}; +use clap::Parser; use std::{ io::{self, IsTerminal, Write}, path::Path, @@ -8,10 +8,15 @@ use std::{ }; use term::{clear_terminal, press_enter_prompt}; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; +use crate::{ + app_state::AppState, + cli::{Args, Command}, + info_file::InfoFile, +}; mod app_state; mod cargo_toml; +mod cli; mod cmd; mod dev; mod embedded; @@ -25,44 +30,6 @@ mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code -#[derive(Parser)] -#[command(version)] -struct Args { - #[command(subcommand)] - command: Option, - /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. - #[arg(long)] - manual_run: bool, -} - -#[derive(Subcommand)] -enum Subcommands { - /// Initialize the official Rustlings exercises - Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified - Run { - /// The name of the exercise - name: Option, - }, - /// Check all the exercises, marking them as done or pending accordingly. - CheckAll, - /// Reset a single exercise - Reset { - /// The name of the exercise - name: String, - }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified - Hint { - /// The name of the exercise - name: Option, - }, - /// Commands for developing (community) Rustlings exercises - #[command(subcommand)] - Dev(DevCommands), -} - fn main() -> Result { let args = Args::parse(); @@ -72,8 +39,8 @@ fn main() -> Result { 'priority_cmd: { match args.command { - Some(Subcommands::Init) => init::init().context("Initialization failed")?, - Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + Some(Command::Init) => init::init().context("Initialization failed")?, + Some(Command::Dev(dev_command)) => dev_command.run()?, _ => break 'priority_cmd, } @@ -141,13 +108,13 @@ fn main() -> Result { watch::watch(&mut app_state, notify_exercise_names)?; } - Some(Subcommands::Run { name }) => { + Some(Command::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } return run::run(&mut app_state); } - Some(Subcommands::CheckAll) => { + Some(Command::CheckAll) => { let mut stdout = io::stdout().lock(); if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? { if app_state.current_exercise().done { @@ -175,19 +142,19 @@ fn main() -> Result { app_state.render_final_message(&mut stdout)?; } - Some(Subcommands::Reset { name }) => { + Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; let exercise_path = app_state.reset_current_exercise()?; println!("The exercise {exercise_path} has been reset"); } - Some(Subcommands::Hint { name }) => { + Some(Command::Hint { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } println!("{}", app_state.current_exercise().hint); } // Handled in an earlier match. - Some(Subcommands::Init | Subcommands::Dev(_)) => (), + Some(Command::Init | Command::Dev(_)) => (), } Ok(ExitCode::SUCCESS) diff --git a/src/watch.rs b/src/watch.rs index 3a56b4b6..e0b5ccd0 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -13,10 +13,9 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, list, + watch::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}, }; -use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; - mod notify_event; mod state; mod terminal_event; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 9c05f10d..edd9c720 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -12,7 +12,7 @@ use std::{ time::Duration, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); diff --git a/src/watch/state.rs b/src/watch/state.rs index f93ea0cf..1b285d67 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -17,10 +17,9 @@ use crate::{ clear_terminal, exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, term::progress_bar, + watch::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}, }; -use super::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}; - const HEADING_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Bold) .with(Attribute::Underlined); diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 439e4730..4f0685b6 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,7 +4,7 @@ use std::sync::{ mpsc::{Receiver, Sender}, }; -use super::{EXERCISE_RUNNING, WatchEvent}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; pub enum InputEvent { Next, From 95b6160b54120515bc538c0c88b40beb0552ab29 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 17:28:48 +0200 Subject: [PATCH 37/78] Don't manually inline --- src/app_state.rs | 7 ------- src/cmd.rs | 2 -- src/exercise.rs | 5 ----- src/info_file.rs | 5 ----- src/list/scroll_state.rs | 4 ---- src/list/state.rs | 5 ----- src/term.rs | 6 ------ src/watch.rs | 2 -- tests/integration_tests.rs | 5 ----- 9 files changed, 41 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5722e607..411afe36 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -180,37 +180,30 @@ impl AppState { Ok((slf, state_file_status)) } - #[inline] pub fn current_exercise_ind(&self) -> usize { self.current_exercise_ind } - #[inline] pub fn exercises(&self) -> &[Exercise] { &self.exercises } - #[inline] pub fn n_done(&self) -> u32 { self.n_done } - #[inline] pub fn n_pending(&self) -> u32 { self.exercises.len() as u32 - self.n_done } - #[inline] pub fn current_exercise(&self) -> &Exercise { &self.exercises[self.current_exercise_ind] } - #[inline] pub fn cmd_runner(&self) -> &CmdRunner { &self.cmd_runner } - #[inline] pub fn emit_file_links(&self) -> bool { self.emit_file_links } diff --git a/src/cmd.rs b/src/cmd.rs index b2c58f6a..6442e449 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -126,7 +126,6 @@ pub struct CargoSubcommand<'out> { } impl CargoSubcommand<'_> { - #[inline] pub fn args<'arg, I>(&mut self, args: I) -> &mut Self where I: IntoIterator, @@ -136,7 +135,6 @@ impl CargoSubcommand<'_> { } /// The boolean in the returned `Result` is true if the command's exit status is success. - #[inline] pub fn run(self, description: &str) -> Result { run_cmd(self.cmd, description, self.output) } diff --git a/src/exercise.rs b/src/exercise.rs index c07a94e8..987428e6 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -158,7 +158,6 @@ pub trait RunnableExercise { /// Compile, check and run the exercise. /// The output is written to the `output` buffer after clearing it. - #[inline] fn run_exercise(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { self.run::(self.name(), output, cmd_runner) } @@ -201,22 +200,18 @@ pub trait RunnableExercise { } impl RunnableExercise for Exercise { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/info_file.rs b/src/info_file.rs index 54a21a5c..26bb1a2c 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -23,7 +23,6 @@ pub struct ExerciseInfo { #[serde(default)] pub skip_check_unsolved: bool, } -#[inline] const fn default_true() -> bool { true } @@ -55,22 +54,18 @@ impl ExerciseInfo { } impl RunnableExercise for ExerciseInfo { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 2c02ed4f..299db568 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -19,7 +19,6 @@ impl ScrollState { } } - #[inline] pub fn offset(&self) -> usize { self.offset } @@ -41,7 +40,6 @@ impl ScrollState { .min(global_max_offset); } - #[inline] pub fn selected(&self) -> Option { self.selected } @@ -86,12 +84,10 @@ impl ScrollState { self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1))); } - #[inline] fn update_scroll_padding(&mut self) { self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding); } - #[inline] pub fn max_n_rows_to_display(&self) -> usize { self.max_n_rows_to_display } diff --git a/src/list/state.rs b/src/list/state.rs index 4e097613..4fcbd3c3 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -303,7 +303,6 @@ impl<'a> ListState<'a> { self.scroll_state.set_n_rows(n_rows); } - #[inline] pub fn filter(&self) -> Filter { self.filter } @@ -313,22 +312,18 @@ impl<'a> ListState<'a> { self.update_rows(); } - #[inline] pub fn select_next(&mut self) { self.scroll_state.select_next(); } - #[inline] pub fn select_previous(&mut self) { self.scroll_state.select_previous(); } - #[inline] pub fn select_first(&mut self) { self.scroll_state.select_first(); } - #[inline] pub fn select_last(&mut self) { self.scroll_state.select_last(); } diff --git a/src/term.rs b/src/term.rs index 8cab5005..2467b450 100644 --- a/src/term.rs +++ b/src/term.rs @@ -18,7 +18,6 @@ pub struct MaxLenWriter<'a, 'lock> { } impl<'a, 'lock> MaxLenWriter<'a, 'lock> { - #[inline] pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self { Self { stdout, @@ -28,7 +27,6 @@ impl<'a, 'lock> MaxLenWriter<'a, 'lock> { } // Additional is for emojis that take more space. - #[inline] pub fn add_to_len(&mut self, additional: usize) { self.len += additional; } @@ -64,24 +62,20 @@ impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> { Ok(()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'lock> { self.stdout } } impl<'a> CountedWrite<'a> for StdoutLock<'a> { - #[inline] fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { self.write_all(ascii) } - #[inline] fn write_str(&mut self, unicode: &str) -> io::Result<()> { self.write_all(unicode.as_bytes()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'a> { self } diff --git a/src/watch.rs b/src/watch.rs index e0b5ccd0..ab96893e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -27,7 +27,6 @@ static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); pub struct InputPauseGuard(()); impl InputPauseGuard { - #[inline] pub fn scoped_pause() -> Self { EXERCISE_RUNNING.store(true, Relaxed); Self(()) @@ -35,7 +34,6 @@ impl InputPauseGuard { } impl Drop for InputPauseGuard { - #[inline] fn drop(&mut self) { EXERCISE_RUNNING.store(false, Relaxed); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 91d0536e..bb1e398c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -19,19 +19,16 @@ struct Cmd<'a> { } impl<'a> Cmd<'a> { - #[inline] fn current_dir(&mut self, current_dir: &'a str) -> &mut Self { self.current_dir = Some(current_dir); self } - #[inline] fn args(&mut self, args: &'a [&'a str]) -> &mut Self { self.args = args; self } - #[inline] fn output(&mut self, output: Output<'a>) -> &mut Self { self.output = Some(output); self @@ -70,13 +67,11 @@ impl<'a> Cmd<'a> { } } - #[inline] #[track_caller] fn success(&self) { self.assert(true); } - #[inline] #[track_caller] fn fail(&self) { self.assert(false); From 7c1d8ebf49f5dcfe4c6bee0fd881a6922cbc0395 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 25 Sep 2025 17:18:26 +0200 Subject: [PATCH 38/78] Make users type method syntax themselves in `structs3` closes #2286 --- CHANGELOG.md | 1 + exercises/07_structs/structs3.rs | 99 +++++++++++--------------------- rustlings-macros/info.toml | 9 ++- solutions/07_structs/structs3.rs | 89 ++++++++++------------------ 4 files changed, 69 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f326b60..61491446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. +- `structs3`: Rewrote the exercise to make users type method syntax themselves. ## 6.5.0 (2025-08-21) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 69e5ced7..b5457de6 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,37 +1,33 @@ // Structs contain data, but can also have logic. In this exercise, we have -// defined the `Package` struct, and we want to test some logic attached to it. +// defined the `Fireworks` struct and a couple of functions that work with it. +// Turn these free-standing functions into methods and associated functions +// to express that relationship more clearly in the code. + +#![deny(clippy::use_self)] // practice using the `Self` type #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } +// TODO: Turn this function into an associated function on `Fireworks`. +fn new_fireworks() -> Fireworks { + Fireworks { rockets: 0 } +} - Self { - sender_country, - recipient_country, - weight_in_grams, - } - } +// TODO: Turn this function into a method on `Fireworks`. +fn add_rockets(fireworks: &mut Fireworks, rockets: usize) { + fireworks.rockets += rockets +} - // TODO: Add the correct return type to the function signature. - fn is_international(&self) { - // TODO: Read the tests that use this method to find out when a package - // is considered international. - } - - // TODO: Add the correct return type to the function signature. - fn get_fees(&self, cents_per_gram: u32) { - // TODO: Calculate the package's fees. +// TODO: Turn this function into a method on `Fireworks`. +fn start(fireworks: Fireworks) -> String { + if fireworks.rockets < 5 { + String::from("small") + } else if fireworks.rockets < 20 { + String::from("medium") + } else { + String::from("big") } } @@ -44,44 +40,19 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "small"); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(15); + assert_eq!(f.start(), "medium"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(100); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "big"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index e42b0f26..353d405c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -417,11 +417,10 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances- name = "structs3" dir = "07_structs" hint = """ -For `is_international`: What makes a package international? Seems related to -the places it goes through right? - -For `get_fees`: This method takes an additional argument, is there a field in -the `Package` struct that this relates to? +Methods and associated functions are both declared in an `impl MyType {}` +block. Methods have a `self`, `&self` or `&mut self` parameter, where `self` +implicitly has the type of the impl block. Associated functions do not have +a `self` parameter. Have a look in The Book to find out more about method implementations: https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index 3f878cc8..a8928443 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -1,33 +1,27 @@ +#![deny(clippy::use_self)] // practice using the `Self` type + #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } - - Self { - sender_country, - recipient_country, - weight_in_grams, - } +impl Fireworks { + fn new() -> Self { + Self { rockets: 0 } } - fn is_international(&self) -> bool { - // ^^^^^^^ added - self.sender_country != self.recipient_country + fn add_rockets(&mut self, rockets: usize) { + self.rockets += rockets } - fn get_fees(&self, cents_per_gram: u32) -> u32 { - // ^^^^^^ added - self.weight_in_grams * cents_per_gram + fn start(self) -> String { + if self.rockets < 5 { + String::from("small") + } else if self.rockets < 20 { + String::from("medium") + } else { + String::from("big") + } } } @@ -40,44 +34,19 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "small"); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(15); + assert_eq!(f.start(), "medium"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(100); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "big"); } } From 4d97c31c0f748acda001e6f0f9c63261ec1d7afe Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 5 Apr 2026 18:17:10 +0200 Subject: [PATCH 39/78] Add Zellij support --- src/app_state.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++- src/cli.rs | 13 +++-- src/main.rs | 1 + src/watch/state.rs | 11 ++-- tmp.txt | 1 + 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 tmp.txt diff --git a/src/app_state.rs b/src/app_state.rs index 411afe36..31053f73 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; +use serde::Deserialize; use std::{ collections::HashSet, env, @@ -11,7 +12,7 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread, + thread::{self, JoinHandle}, }; use crate::{ @@ -49,6 +50,44 @@ pub enum CheckProgress { Pending, } +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +#[must_use] +pub struct EditCmdJoinHandle(Option>>); + +fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -61,12 +100,15 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, + zellij: bool, + open_pane: Option<(String, u32, usize)>, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, + zellij: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -175,6 +217,8 @@ impl AppState { cmd_runner, // VS Code has its own file link handling emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), + zellij, + open_pane: None, }; Ok((slf, state_file_status)) @@ -553,6 +597,86 @@ impl AppState { Ok(()) } + + pub fn close_pane(&mut self) -> Result<()> { + if let Some((pane_id_str, _, _)) = self.open_pane.take() { + close_pane(&pane_id_str)?; + } + + Ok(()) + } + + pub fn edit_cmd(&mut self) -> Result { + if !self.zellij { + return Ok(EditCmdJoinHandle(None)); + } + + let open_pane = self.open_pane.take(); + let current_exercise_ind = self.current_exercise_ind; + let mut edit_cmd = Command::new("zellij"); + edit_cmd + .arg("action") + .arg("edit") + .arg(&self.current_exercise().path) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + + let handle = thread::Builder::new() + .spawn(move || { + if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { + if exercise_ind == current_exercise_ind { + // Check if the pane is still open + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context( + "Failed to parse the output of `zellij action list-panes -j`", + )?; + + if panes.iter().any(|pane| pane.id == pane_id) { + return Ok((pane_id_str, pane_id)); + } + } else { + close_pane(&pane_id_str)?; + } + } + + let output = edit_cmd.output()?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane") + }) + .context("Failed to spawn a thread to open and close Zellij panes")?; + + Ok(EditCmdJoinHandle(Some(handle))) + } + + pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { + if let Some(handle) = handle.0 { + let (pane_id_str, pane_id) = handle.join().unwrap()?; + self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + } + + Ok(()) + } } const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -608,6 +732,8 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, + zellij: false, + open_pane: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index 2bea5544..dbc31f9c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,28 +9,33 @@ pub struct Args { #[command(subcommand)] pub command: Option, /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. + /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, + /// Open the current exercise in a new Zellij pane and close the last one if exists + #[arg(long)] + pub zellij: bool, } #[derive(Subcommand)] pub enum Command { /// Initialize the official Rustlings exercises Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified + /// Run a single exercise. + /// Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise name: Option, }, - /// Check all the exercises, marking them as done or pending accordingly. + /// Check all the exercises, marking them as done or pending accordingly CheckAll, /// Reset a single exercise Reset { /// The name of the exercise name: String, }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified + /// Show a hint. + /// Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise name: Option, diff --git a/src/main.rs b/src/main.rs index 8f31703b..29ec3d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + args.zellij, )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 1b285d67..9e7d1ddc 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -78,14 +78,16 @@ impl<'a> WatchState<'a> { // Ignore any input until running the exercise is done. let _input_pause_guard = InputPauseGuard::scoped_pause(); - self.show_hint = false; - writeln!( stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; + let edit_cmd_handle = self.app_state.edit_cmd()?; + + self.show_hint = false; + let success = self .app_state .current_exercise() @@ -105,7 +107,9 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } + self.app_state.join_edit_cmd(edit_cmd_handle)?; self.render(stdout)?; + Ok(()) } @@ -127,9 +131,10 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { + self.app_state.close_pane()?; self.app_state.reset_current_exercise()?; - // The file watcher reruns the exercise otherwise. + // The file watcher reruns the exercise otherwise if self.manual_run { self.run_current_exercise(stdout)?; } diff --git a/tmp.txt b/tmp.txt new file mode 100644 index 00000000..3aefeefc --- /dev/null +++ b/tmp.txt @@ -0,0 +1 @@ +226.867688ms \ No newline at end of file From c9ccedcff6a40f6b77c1b608ef0d3942215138c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 14:01:34 +0200 Subject: [PATCH 40/78] Support VSCode and --edit-cmd as editor --- src/app_state.rs | 150 ++++++++----------------------------------- src/cli.rs | 7 +- src/editor.rs | 137 +++++++++++++++++++++++++++++++++++++++ src/editor/zellij.rs | 62 ++++++++++++++++++ src/exercise.rs | 6 +- src/main.rs | 4 +- src/watch/state.rs | 6 +- 7 files changed, 238 insertions(+), 134 deletions(-) create mode 100644 src/editor.rs create mode 100644 src/editor/zellij.rs diff --git a/src/app_state.rs b/src/app_state.rs index 31053f73..bc1d520d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,7 @@ use anyhow::{Context, Error, Result, bail}; use crossterm::{QueueableCommand, cursor, terminal}; -use serde::Deserialize; use std::{ collections::HashSet, - env, fs::{File, OpenOptions}, io::{Read, Seek, StdoutLock, Write}, path::{MAIN_SEPARATOR_STR, Path}, @@ -12,12 +10,13 @@ use std::{ atomic::{AtomicUsize, Ordering::Relaxed}, mpsc, }, - thread::{self, JoinHandle}, + thread, }; use crate::{ clear_terminal, cmd::CmdRunner, + editor::{Editor, EditorJoinHandle}, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -50,44 +49,6 @@ pub enum CheckProgress { Pending, } -#[derive(Deserialize)] -struct Pane { - id: u32, -} - -#[must_use] -pub struct EditCmdJoinHandle(Option>>); - -fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { - // Remove newline - let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; - let id_str = str::from_utf8(b).ok()?; - - let (first, rest) = b.split_first()?; - let mut id = u32::from(first - b'0'); - - for c in rest { - id = 10 * id + u32::from(c - b'0'); - } - - Some((id_str.to_owned(), id)) -} - -fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; - - Ok(()) -} - pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -100,15 +61,14 @@ pub struct AppState { official_exercises: bool, cmd_runner: CmdRunner, emit_file_links: bool, - zellij: bool, - open_pane: Option<(String, u32, usize)>, + editor: Option, } impl AppState { pub fn new( exercise_infos: Vec, final_message: &'static str, - zellij: bool, + editor: Option, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -150,7 +110,9 @@ impl AppState { Exercise { name: exercise_info.name, dir: exercise_info.dir, - path: exercise_info.path(), + // Leaking for `Editor::open`. + // Leaking is fine since the app state exists until the end of the program. + path: exercise_info.path().leak(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, @@ -216,9 +178,8 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), - zellij, - open_pane: None, + emit_file_links: !matches!(editor, Some(Editor::VSCode)), + editor, }; Ok((slf, state_file_status)) @@ -376,9 +337,9 @@ impl AppState { pub fn reset_current_exercise(&mut self) -> Result<&str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, &exercise.path)?; + self.reset(self.current_exercise_ind, exercise.path)?; - Ok(&exercise.path) + Ok(exercise.path) } // Reset the exercise by index and return its name. @@ -389,7 +350,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, &exercise.path)?; + self.reset(exercise_ind, exercise.path)?; Ok(exercise.name) } @@ -598,81 +559,23 @@ impl AppState { Ok(()) } - pub fn close_pane(&mut self) -> Result<()> { - if let Some((pane_id_str, _, _)) = self.open_pane.take() { - close_pane(&pane_id_str)?; + pub fn open_editor(&mut self) -> Result { + if let Some(editor) = self.editor.take() { + return editor.open(self.current_exercise_ind, self.current_exercise().path); } + Ok(EditorJoinHandle::default()) + } + + pub fn join_editor_handle(&mut self, handle: EditorJoinHandle) -> Result<()> { + self.editor = handle.join()?; + Ok(()) } - pub fn edit_cmd(&mut self) -> Result { - if !self.zellij { - return Ok(EditCmdJoinHandle(None)); - } - - let open_pane = self.open_pane.take(); - let current_exercise_ind = self.current_exercise_ind; - let mut edit_cmd = Command::new("zellij"); - edit_cmd - .arg("action") - .arg("edit") - .arg(&self.current_exercise().path) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - - let handle = thread::Builder::new() - .spawn(move || { - if let Some((pane_id_str, pane_id, exercise_ind)) = open_pane { - if exercise_ind == current_exercise_ind { - // Check if the pane is still open - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } - - // Remove newline - output.stdout.pop(); - - let panes = serde_json::de::from_slice::>(&output.stdout) - .context( - "Failed to parse the output of `zellij action list-panes -j`", - )?; - - if panes.iter().any(|pane| pane.id == pane_id) { - return Ok((pane_id_str, pane_id)); - } - } else { - close_pane(&pane_id_str)?; - } - } - - let output = edit_cmd.output()?; - - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } - - parse_pane_id(&output.stdout) - .context("Failed to parse the ID of the new Zellij pane") - }) - .context("Failed to spawn a thread to open and close Zellij panes")?; - - Ok(EditCmdJoinHandle(Some(handle))) - } - - pub fn join_edit_cmd(&mut self, handle: EditCmdJoinHandle) -> Result<()> { - if let Some(handle) = handle.0 { - let (pane_id_str, pane_id) = handle.join().unwrap()?; - self.open_pane = Some((pane_id_str, pane_id, self.current_exercise_ind)); + pub fn close_editor(&mut self) -> Result<()> { + if let Some(editor) = &mut self.editor { + editor.close()?; } Ok(()) @@ -711,7 +614,7 @@ mod tests { Exercise { name: "0", dir: None, - path: String::from("exercises/0.rs"), + path: "exercises/0.rs", canonical_path: None, test: false, strict_clippy: false, @@ -732,8 +635,7 @@ mod tests { official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), emit_file_links: true, - zellij: false, - open_pane: None, + editor: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cli.rs b/src/cli.rs index dbc31f9c..8bbe25aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,13 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. + /// Ignored in VS Code + #[arg(long)] + pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, - /// Open the current exercise in a new Zellij pane and close the last one if exists - #[arg(long)] - pub zellij: bool, } #[derive(Subcommand)] diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 00000000..be24e3ef --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,137 @@ +use std::{ + env, + process::{Command, Stdio}, + thread::{self, JoinHandle}, +}; + +use anyhow::{Context, Result, bail}; + +mod zellij; + +pub enum Editor { + VSCode, + Cmd(String, Vec), + Zellij(Option<(String, u32, usize)>), +} + +impl Editor { + pub fn new(cmd: Option) -> Option { + if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { + return Some(Self::VSCode); + } + + if let Some(cmd) = cmd { + todo!() + } + + if env::var_os("ZELLIJ").is_some() { + return Some(Self::Zellij(None)); + } + + None + } + + pub fn open( + self, + exercise_ind: usize, + exercise_path: &'static str, + ) -> Result { + let handle = thread::Builder::new() + .spawn(move || match self { + Editor::VSCode => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `code` to open the current exercise file")? + .success() + { + bail!("Failed to run `code PATH` to open the current exercise file"); + } + + Ok(Self::VSCode) + } + Editor::Cmd(program, args) => { + if !Command::new("code") + .arg(exercise_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run the command from `--edit-cmd`") + .is_ok_and(|status| status.success()) + { + bail!("Failed to run the command from `--edit-cmd`"); + } + + Ok(Self::Cmd(program, args)) + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if open_exercise_ind == exercise_ind { + if zellij::pane_open(pane_id)? { + return Ok(Self::Zellij(Some(( + pane_id_str, + pane_id, + exercise_ind, + )))); + } + } else { + zellij::close_pane(&pane_id_str)?; + } + } + + let output = Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij`")?; + + if !output.status.success() { + bail!("Failed to open a new Zellij editor pane"); + } + + let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + .context("Failed to parse the ID of the new Zellij pane")?; + + Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + } + }) + .context("Failed to spawn a thread to open the editor")?; + + Ok(EditorJoinHandle(Some(handle))) + } + + pub fn close(&mut self) -> Result<()> { + match self { + Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, _, _)) = open_pane.take() { + zellij::close_pane(&pane_id_str)?; + } + } + } + + Ok(()) + } +} + +#[must_use] +#[derive(Default)] +pub struct EditorJoinHandle(Option>>); + +impl EditorJoinHandle { + pub fn join(self) -> Result> { + if let Some(handle) = self.0 { + let editor = handle.join().unwrap()?; + return Ok(Some(editor)); + } + + Ok(None) + } +} diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs new file mode 100644 index 00000000..ffa906dc --- /dev/null +++ b/src/editor/zellij.rs @@ -0,0 +1,62 @@ +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result, bail}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +pub fn pane_open(pane_id: u32) -> Result { + let mut output = Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .context("Failed to run `zellij action list-panes -j`")?; + + if !output.status.success() { + bail!("`zellij action list-panes -j` didn't exit successfully"); + } + + // Remove newline + output.stdout.pop(); + + let panes = serde_json::de::from_slice::>(&output.stdout) + .context("Failed to parse the output of `zellij action list-panes -j`")?; + + Ok(panes.iter().any(|pane| pane.id == pane_id)) +} + +pub fn close_pane(pane_id: &str) -> Result<()> { + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run `zellij action close-pane -p ID`")?; + + Ok(()) +} diff --git a/src/exercise.rs b/src/exercise.rs index 987428e6..b969c69a 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,7 @@ pub struct Exercise { pub name: &'static str, pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. - pub path: String, + pub path: &'static str, pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, @@ -85,9 +85,9 @@ impl Exercise { ) -> io::Result<()> { file_path(writer, Color::Blue, |writer| { if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { - terminal_file_link(writer, &self.path, canonical_path) + terminal_file_link(writer, self.path, canonical_path) } else { - writer.write_str(&self.path) + writer.write_str(self.path) } }) } diff --git a/src/main.rs b/src/main.rs index 29ec3d01..1d6c98b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use term::{clear_terminal, press_enter_prompt}; use crate::{ app_state::AppState, cli::{Args, Command}, + editor::Editor, info_file::InfoFile, }; @@ -19,6 +20,7 @@ mod cargo_toml; mod cli; mod cmd; mod dev; +mod editor; mod embedded; mod exercise; mod info_file; @@ -61,7 +63,7 @@ fn main() -> Result { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - args.zellij, + Editor::new(args.edit_cmd), )?; // Show the welcome message if the state file doesn't exist yet. diff --git a/src/watch/state.rs b/src/watch/state.rs index 9e7d1ddc..8bbdc585 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -84,7 +84,7 @@ impl<'a> WatchState<'a> { self.app_state.current_exercise().name, )?; - let edit_cmd_handle = self.app_state.edit_cmd()?; + let editor_handle = self.app_state.open_editor()?; self.show_hint = false; @@ -107,7 +107,7 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } - self.app_state.join_edit_cmd(edit_cmd_handle)?; + self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; Ok(()) @@ -131,7 +131,7 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { - self.app_state.close_pane()?; + self.app_state.close_editor()?; self.app_state.reset_current_exercise()?; // The file watcher reruns the exercise otherwise From dace3e39534f31adc2ef95a036fc8aee1fbaf004 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:12:49 +0200 Subject: [PATCH 41/78] Add run_cmd --- src/editor.rs | 63 ++++++++++++++++++++------------------------ src/editor/zellij.rs | 45 +++++++++++++------------------ 2 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index be24e3ef..af5c2fbe 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -8,6 +8,25 @@ use anyhow::{Context, Result, bail}; mod zellij; +fn run_cmd(cmd: &mut Command) -> Result> { + let output = cmd + .stdin(Stdio::null()) + .output() + .with_context(|| format!("Failed to run the command {cmd:?}"))?; + + if !output.status.success() { + bail!( + "The command {cmd:?} didn't run successfully\n\n\ + stdout:\n{}\n\n\ + stderr:\n{}", + str::from_utf8(&output.stdout).unwrap_or_default(), + str::from_utf8(&output.stderr).unwrap_or_default(), + ); + } + + Ok(output.stdout) +} + pub enum Editor { VSCode, Cmd(String, Vec), @@ -39,32 +58,12 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || match self { Editor::VSCode => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `code` to open the current exercise file")? - .success() - { - bail!("Failed to run `code PATH` to open the current exercise file"); - } + run_cmd(Command::new("code").arg(exercise_path))?; Ok(Self::VSCode) } Editor::Cmd(program, args) => { - if !Command::new("code") - .arg(exercise_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run the command from `--edit-cmd`") - .is_ok_and(|status| status.success()) - { - bail!("Failed to run the command from `--edit-cmd`"); - } + run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; Ok(Self::Cmd(program, args)) } @@ -83,20 +82,14 @@ impl Editor { } } - let output = Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij`")?; + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - if !output.status.success() { - bail!("Failed to open a new Zellij editor pane"); - } - - let (pane_id_str, pane_id) = zellij::parse_pane_id(&output.stdout) + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) .context("Failed to parse the ID of the new Zellij pane")?; Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs index ffa906dc..b628a682 100644 --- a/src/editor/zellij.rs +++ b/src/editor/zellij.rs @@ -1,8 +1,10 @@ -use std::process::{Command, Stdio}; +use std::process::Command; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use serde::Deserialize; +use crate::editor::run_cmd; + #[derive(Deserialize)] struct Pane { id: u32, @@ -24,39 +26,30 @@ pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { } pub fn pane_open(pane_id: u32) -> Result { - let mut output = Command::new("zellij") - .arg("action") - .arg("list-panes") - .arg("-j") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .context("Failed to run `zellij action list-panes -j`")?; - - if !output.status.success() { - bail!("`zellij action list-panes -j` didn't exit successfully"); - } + let mut stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j"), + )?; // Remove newline - output.stdout.pop(); + stdout.pop(); - let panes = serde_json::de::from_slice::>(&output.stdout) + let panes = serde_json::de::from_slice::>(&stdout) .context("Failed to parse the output of `zellij action list-panes -j`")?; Ok(panes.iter().any(|pane| pane.id == pane_id)) } pub fn close_pane(pane_id: &str) -> Result<()> { - Command::new("zellij") - .arg("action") - .arg("close-pane") - .arg("-p") - .arg(pane_id) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("Failed to run `zellij action close-pane -p ID`")?; + run_cmd( + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id), + )?; Ok(()) } From b48663030b492abf335a3338b5c8bd00613a0bdd Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 16:55:10 +0200 Subject: [PATCH 42/78] Add shlex --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/editor.rs | 17 ++++++++++++----- src/main.rs | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c2d6d75..b8db4360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,7 @@ dependencies = [ "rustlings-macros", "serde", "serde_json", + "shlex", "tempfile", "toml", ] @@ -571,6 +572,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 068a3f49..192eeb61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ notify = "8" rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } serde_json = "1" serde.workspace = true +shlex = "1" toml.workspace = true [target.'cfg(not(windows))'.dependencies] diff --git a/src/editor.rs b/src/editor.rs index af5c2fbe..6e712605 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::{Context, Result, bail}; +use shlex::Shlex; mod zellij; @@ -34,20 +35,26 @@ pub enum Editor { } impl Editor { - pub fn new(cmd: Option) -> Option { + pub fn new(cmd: Option) -> Result> { if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Some(Self::VSCode); + return Ok(Some(Self::VSCode)); } if let Some(cmd) = cmd { - todo!() + let shlex = &mut Shlex::new(&cmd); + let program = shlex.next().context("Program missing in `--edit-cmd`")?; + let args = shlex.collect(); + if shlex.had_error { + bail!("Failed to parse the command in `--edit-cmd`"); + } + return Ok(Some(Self::Cmd(program, args))); } if env::var_os("ZELLIJ").is_some() { - return Some(Self::Zellij(None)); + return Ok(Some(Self::Zellij(None))); } - None + Ok(None) } pub fn open( diff --git a/src/main.rs b/src/main.rs index 1d6c98b1..7cd5c80b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,10 +60,11 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } + let editor = Editor::new(args.edit_cmd)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - Editor::new(args.edit_cmd), + editor, )?; // Show the welcome message if the state file doesn't exist yet. From b0dc0140406ef00ce3eb41d0a9adb8fca8f1539b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:24:43 +0200 Subject: [PATCH 43/78] Improve description of --edit-cmd --- src/cli.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8bbe25aa..5830cbed 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,8 +8,14 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, - /// Open the current exercise by running the provided `EDIT_CMD EXERCISE_NAME`. - /// Ignored in VS Code + /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. + /// The command is not allowed to block (e.g. `vim`). + /// It should communicate with an editor in a different process. + /// `EDIT_CMD` can contain arguments like `--edit-cmd "PROGRAM -x --arg1"`. + /// The current exercise's path is added by Rustlings as the last argument. + /// `--edit-cmd` is ignored in VS Code. + /// + /// Example: `--edit-cmd "code"` (default behavior if running in a VS Code terminal) #[arg(long)] pub edit_cmd: Option, /// Manually run the current exercise using `r` in the watch mode. From bc0b4e9f9a716e577e80667b2b1521b55724d71b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 17:27:35 +0200 Subject: [PATCH 44/78] Simplify Editor::open --- src/editor.rs | 64 ++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/editor.rs b/src/editor.rs index 6e712605..3f36e266 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -58,49 +58,45 @@ impl Editor { } pub fn open( - self, + mut self, exercise_ind: usize, exercise_path: &'static str, ) -> Result { let handle = thread::Builder::new() - .spawn(move || match self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - - Ok(Self::VSCode) - } - Editor::Cmd(program, args) => { - run_cmd(Command::new(&program).args(&args).arg(exercise_path))?; - - Ok(Self::Cmd(program, args)) - } - Editor::Zellij(open_pane) => { - if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { - if open_exercise_ind == exercise_ind { - if zellij::pane_open(pane_id)? { - return Ok(Self::Zellij(Some(( - pane_id_str, - pane_id, - exercise_ind, - )))); - } - } else { - zellij::close_pane(&pane_id_str)?; - } + .spawn(move || { + match &mut self { + Editor::VSCode => { + run_cmd(Command::new("code").arg(exercise_path))?; } + Editor::Cmd(program, args) => { + run_cmd(Command::new(program).args(args).arg(exercise_path))?; + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if *open_exercise_ind == exercise_ind { + if zellij::pane_open(*pane_id)? { + return Ok(self); + } + } else { + zellij::close_pane(pane_id_str)?; + } + } - let stdout = run_cmd( - Command::new("zellij") - .arg("action") - .arg("edit") - .arg(exercise_path), - )?; + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; - let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) - .context("Failed to parse the ID of the new Zellij pane")?; + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) + .context("Failed to parse the ID of the new Zellij pane")?; - Ok(Self::Zellij(Some((pane_id_str, pane_id, exercise_ind)))) + *open_pane = Some((pane_id_str, pane_id, exercise_ind)); + } } + + Ok(self) }) .context("Failed to spawn a thread to open the editor")?; From 95499f18dd7cb0811dd304acd051e493fa8661d6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:11:13 +0200 Subject: [PATCH 45/78] Close editor on quit --- src/main.rs | 1 + src/watch.rs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7cd5c80b..652d1468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,7 @@ fn main() -> Result { }; watch::watch(&mut app_state, notify_exercise_names)?; + app_state.close_editor()?; } Some(Command::Run { name }) => { if let Some(name) = name { diff --git a/src/watch.rs b/src/watch.rs index ab96893e..857ccfde 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -172,8 +172,7 @@ pub fn watch( watch_list_loop(app_state, notify_exercise_names) } -const QUIT_MSG: &[u8] = b" - +const QUIT_MSG: &[u8] = b"q\n We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory. "; From f403d9e1b68b601a5b6e9e57201f767b134e2e67 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:21:15 +0200 Subject: [PATCH 46/78] Show current exercise on hint command --- CHANGELOG.md | 4 ++++ src/main.rs | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9b356b..b85a3e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Show the file link of the current exercise when running `rustlings hint` + ### Fixed - Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) diff --git a/src/main.rs b/src/main.rs index 652d1468..564e0719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,15 @@ fn main() -> Result { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - println!("{}", app_state.current_exercise().hint); + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Current exercise: ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + + stdout.write_all(b"\n\nHint:\n")?; + stdout.write_all(current_exercise.hint.as_bytes())?; + stdout.write_all(b"\n")?; } // Handled in an earlier match. Some(Command::Init | Command::Dev(_)) => (), From 695f927893ae4fc1f1aaa629e2feb62a9cdcc78d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:26:36 +0200 Subject: [PATCH 47/78] Show file link on reset command --- CHANGELOG.md | 2 +- src/app_state.rs | 6 ++---- src/main.rs | 9 +++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85a3e0b..ec74cf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Show the file link of the current exercise when running `rustlings hint` +- Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index bc1d520d..9980aeea 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -334,12 +334,10 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&str> { + pub fn reset_current_exercise(&mut self) -> Result<()> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; - - Ok(exercise.path) + self.reset(self.current_exercise_ind, exercise.path) } // Reset the exercise by index and return its name. diff --git a/src/main.rs b/src/main.rs index 564e0719..fb976653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,8 +149,13 @@ fn main() -> Result { } Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - let exercise_path = app_state.reset_current_exercise()?; - println!("The exercise {exercise_path} has been reset"); + app_state.reset_current_exercise()?; + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"The exercise ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + stdout.write_all(b" has been reset\n")?; } Some(Command::Hint { name }) => { if let Some(name) = name { From b5fbf59c0c79a78e06d0fffd9db86abf0774e0f6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 6 Apr 2026 23:55:30 +0200 Subject: [PATCH 48/78] Check if editor program exists before choosing it --- CHANGELOG.md | 3 +++ src/app_state.rs | 3 ++- src/editor.rs | 35 +++++++++++++++++++++++------------ src/main.rs | 5 ++++- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec74cf10..0d2f003f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- Automatically open the current file if Rustlings is running in a VS Code terminal +- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/app_state.rs b/src/app_state.rs index 9980aeea..78b9c205 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -69,6 +69,7 @@ impl AppState { exercise_infos: Vec, final_message: &'static str, editor: Option, + vs_code_term: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -178,7 +179,7 @@ impl AppState { official_exercises: !Path::new("info.toml").exists(), cmd_runner, // VS Code has its own file link handling - emit_file_links: !matches!(editor, Some(Editor::VSCode)), + emit_file_links: !vs_code_term, editor, }; diff --git a/src/editor.rs b/src/editor.rs index 3f36e266..3c189c78 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, env, process::{Command, Stdio}, thread::{self, JoinHandle}, @@ -28,16 +29,29 @@ fn run_cmd(cmd: &mut Command) -> Result> { Ok(output.stdout) } +fn program_exists(program: &str) -> bool { + Command::new(program) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + pub enum Editor { - VSCode, - Cmd(String, Vec), + Cmd(Cow<'static, str>, Vec), Zellij(Option<(String, u32, usize)>), } impl Editor { - pub fn new(cmd: Option) -> Result> { - if env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode") { - return Ok(Some(Self::VSCode)); + pub fn new(cmd: Option, vs_code_term: bool) -> Result> { + if vs_code_term { + for program in ["code", "codium"] { + if program_exists(program) { + return Ok(Some(Self::Cmd(Cow::Borrowed(program), Vec::new()))); + } + } } if let Some(cmd) = cmd { @@ -47,10 +61,10 @@ impl Editor { if shlex.had_error { bail!("Failed to parse the command in `--edit-cmd`"); } - return Ok(Some(Self::Cmd(program, args))); + return Ok(Some(Self::Cmd(Cow::Owned(program), args))); } - if env::var_os("ZELLIJ").is_some() { + if env::var_os("ZELLIJ").is_some() && program_exists("zellij") { return Ok(Some(Self::Zellij(None))); } @@ -65,11 +79,8 @@ impl Editor { let handle = thread::Builder::new() .spawn(move || { match &mut self { - Editor::VSCode => { - run_cmd(Command::new("code").arg(exercise_path))?; - } Editor::Cmd(program, args) => { - run_cmd(Command::new(program).args(args).arg(exercise_path))?; + run_cmd(Command::new(&**program).args(args).arg(exercise_path))?; } Editor::Zellij(open_pane) => { if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { @@ -105,7 +116,7 @@ impl Editor { pub fn close(&mut self) -> Result<()> { match self { - Editor::VSCode | Editor::Cmd(_, _) => (), + Editor::Cmd(_, _) => (), Editor::Zellij(open_pane) => { if let Some((pane_id_str, _, _)) = open_pane.take() { zellij::close_pane(&pane_id_str)?; diff --git a/src/main.rs b/src/main.rs index fb976653..0fa9b75d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; use clap::Parser; use std::{ + env, io::{self, IsTerminal, Write}, path::Path, process::ExitCode, @@ -60,11 +61,13 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } - let editor = Editor::new(args.edit_cmd)?; + let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); + let editor = Editor::new(args.edit_cmd, vs_code_term)?; let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), editor, + vs_code_term, )?; // Show the welcome message if the state file doesn't exist yet. From 432d1f84ea68d21e865215281764094a73236da0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:07:41 +0200 Subject: [PATCH 49/78] Add --no-editor --- CHANGELOG.md | 1 + src/cli.rs | 4 ++++ src/main.rs | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2f003f..cdcdbb14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Automatically open the current file if Rustlings is running in a VS Code terminal - Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij - New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` diff --git a/src/cli.rs b/src/cli.rs index 5830cbed..153994be 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,10 @@ use crate::dev::DevCommand; pub struct Args { #[command(subcommand)] pub command: Option, + /// Disable automatic opening of the current file in VS Code or Zellij. + /// Ignores `--edit-cmd` + #[arg(long)] + pub no_editor: bool, /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. /// The command is not allowed to block (e.g. `vim`). /// It should communicate with an editor in a different process. diff --git a/src/main.rs b/src/main.rs index 0fa9b75d..8da36f7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,12 @@ fn main() -> Result { } let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); - let editor = Editor::new(args.edit_cmd, vs_code_term)?; + let editor = if args.no_editor { + None + } else { + Editor::new(args.edit_cmd, vs_code_term)? + }; + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), From a307599b0bce21bd8c14741fba81d3d076f99b14 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 7 Apr 2026 00:15:24 +0200 Subject: [PATCH 50/78] Fix test --- tests/integration_tests.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb1e398c..d38d4e93 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -4,7 +4,6 @@ use std::{ }; enum Output<'a> { - FullStdout(&'a [u8]), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -47,9 +46,6 @@ impl<'a> Cmd<'a> { let output = cmd.output().unwrap(); match self.output { None => (), - Some(FullStdout(stdout)) => { - assert_eq!(output.stdout, stdout); - } Some(PartialStdout(stdout)) => { assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); } @@ -129,7 +125,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout(b"The answer to everything: 42\n")) + .output(PartialStdout("\n\nHint:\nThe answer to everything: 42\n")) .success(); } From b59f444bbc83231ec52cd2a9bf80bdc7a0fe5755 Mon Sep 17 00:00:00 2001 From: k7a-tomohiro <83070078+k7a-tomohiro@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:26:23 +0900 Subject: [PATCH 51/78] remove --- tmp.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tmp.txt diff --git a/tmp.txt b/tmp.txt deleted file mode 100644 index 3aefeefc..00000000 --- a/tmp.txt +++ /dev/null @@ -1 +0,0 @@ -226.867688ms \ No newline at end of file From 346753b673e0ee0217fe40fcc0ebb21d9c0b0cfc Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 13 Apr 2026 18:51:38 +0200 Subject: [PATCH 52/78] Make starting fireworks more fun :) --- exercises/07_structs/structs3.rs | 21 +++++++-------------- solutions/07_structs/structs3.rs | 21 +++++++-------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index b5457de6..d42729e1 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -22,13 +22,7 @@ fn add_rockets(fireworks: &mut Fireworks, rockets: usize) { // TODO: Turn this function into a method on `Fireworks`. fn start(fireworks: Fireworks) -> String { - if fireworks.rockets < 5 { - String::from("small") - } else if fireworks.rockets < 20 { - String::from("medium") - } else { - String::from("big") - } + "πŸš€".repeat(fireworks.rockets) } fn main() { @@ -41,18 +35,17 @@ mod tests { #[test] fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); + let mut f = Fireworks::new(); f.add_rockets(3); - assert_eq!(f.start(), "small"); + assert_eq!(f.start(), "πŸš€πŸš€πŸš€"); let mut f = Fireworks::new(); - f.add_rockets(15); - assert_eq!(f.start(), "medium"); - - let mut f = Fireworks::new(); - f.add_rockets(100); + f.add_rockets(7); // We don't use method syntax in the last test to ensure the `start` // function takes ownership of the fireworks. - assert_eq!(Fireworks::start(f), "big"); + assert_eq!(Fireworks::start(f), "πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€"); } } diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index a8928443..c672bb19 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -15,13 +15,7 @@ impl Fireworks { } fn start(self) -> String { - if self.rockets < 5 { - String::from("small") - } else if self.rockets < 20 { - String::from("medium") - } else { - String::from("big") - } + "πŸš€".repeat(self.rockets) } } @@ -35,18 +29,17 @@ mod tests { #[test] fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); + let mut f = Fireworks::new(); f.add_rockets(3); - assert_eq!(f.start(), "small"); + assert_eq!(f.start(), "πŸš€πŸš€πŸš€"); let mut f = Fireworks::new(); - f.add_rockets(15); - assert_eq!(f.start(), "medium"); - - let mut f = Fireworks::new(); - f.add_rockets(100); + f.add_rockets(7); // We don't use method syntax in the last test to ensure the `start` // function takes ownership of the fireworks. - assert_eq!(Fireworks::start(f), "big"); + assert_eq!(Fireworks::start(f), "πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€"); } } From 5464fcd7e60c3cfd255faef3165eb65081da7b00 Mon Sep 17 00:00:00 2001 From: Tyler Breisacher Date: Thu, 16 Apr 2026 19:21:08 -0700 Subject: [PATCH 53/78] Add a note about offline stdlib docs --- website/content/setup/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/content/setup/index.md b/website/content/setup/index.md index 54551ada..a67e2809 100644 --- a/website/content/setup/index.md +++ b/website/content/setup/index.md @@ -73,6 +73,10 @@ While working with Rustlings, please use a modern terminal for the best user exp The default terminal on Linux and Mac should be sufficient. On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). +### Offline documentation (optional) + +If you are going to be working on Rustlings while offline, you may want to run `rustup doc --std` to make the standard library documentation available on your machine. + ## Usage After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings πŸš€ From 03c5baf35c4d36c4a587a3bdab19366604c95aed Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 17 Apr 2026 14:56:21 +0200 Subject: [PATCH 54/78] Emphasize hotkeys in footer with color --- src/list/state.rs | 53 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 4fcbd3c3..6e52eec1 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,15 +233,38 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; + let hotkey = |writer: &mut MaxLenWriter, hotkey| -> io::Result<()> { + writer + .stdout + .queue(SetForegroundColor(Color::Yellow))? + .queue(SetAttribute(Attribute::Bold))?; + writer.write_ascii(hotkey)?; + writer.stdout.queue(ResetColor)?; + Ok(()) + }; + let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message if self.scroll_state.selected().is_some() { - writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; + writer.write_str("↓/")?; + hotkey(&mut writer, b"j")?; + writer.write_str(" ↑/")?; + hotkey(&mut writer, b"k")?; + writer.write_ascii(b" home/")?; + hotkey(&mut writer, b"g")?; + writer.write_ascii(b" end/")?; + hotkey(&mut writer, b"G")?; + writer.write_ascii(b" | ")?; + hotkey(&mut writer, b"c")?; + writer.write_ascii(b"ontinue at | ")?; + hotkey(&mut writer, b"r")?; + writer.write_ascii(b"eset exercise")?; next_ln(stdout)?; writer = MaxLenWriter::new(stdout, self.term_width as usize); - writer.write_ascii(b"earch | filter ")?; + hotkey(&mut writer, b"s")?; + writer.write_ascii(b"earch | filter ")?; } else { // Nothing selected (and nothing shown), so only display filter and quit. writer.write_ascii(b"filter ")?; @@ -249,27 +272,41 @@ impl<'a> ListState<'a> { match self.filter { Filter::Done => { + writer.stdout.queue(SetAttribute(Attribute::Underlined))?; + hotkey(&mut writer, b"d")?; writer .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - writer.write_ascii(b"one")?; + writer.write_str("one")?; writer.stdout.queue(ResetColor)?; - writer.write_ascii(b"/

ending")?; + writer.write_ascii(b"/")?; + hotkey(&mut writer, b"p")?; + writer.write_ascii(b"ending")?; } Filter::Pending => { - writer.write_ascii(b"one/")?; + hotkey(&mut writer, b"d")?; + writer.write_ascii(b"one/")?; + writer.stdout.queue(SetAttribute(Attribute::Underlined))?; + hotkey(&mut writer, b"p")?; writer .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - writer.write_ascii(b"

ending")?; + writer.write_ascii(b"ending")?; writer.stdout.queue(ResetColor)?; } - Filter::None => writer.write_ascii(b"one/

ending")?, + Filter::None => { + hotkey(&mut writer, b"d")?; + writer.write_ascii(b"one/")?; + hotkey(&mut writer, b"p")?; + writer.write_ascii(b"ending")?; + } } - writer.write_ascii(b" | uit list")?; + writer.write_ascii(b" | ")?; + hotkey(&mut writer, b"q")?; + writer.write_ascii(b"uit list")?; } else { writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.write_str(&self.message)?; From 013a88a1e629213bf060c6acf798afd5e4f9b9f1 Mon Sep 17 00:00:00 2001 From: Tyler Breisacher Date: Fri, 17 Apr 2026 21:05:13 -0700 Subject: [PATCH 55/78] new wording from @senekor --- website/content/setup/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/setup/index.md b/website/content/setup/index.md index a67e2809..8147ff56 100644 --- a/website/content/setup/index.md +++ b/website/content/setup/index.md @@ -73,9 +73,9 @@ While working with Rustlings, please use a modern terminal for the best user exp The default terminal on Linux and Mac should be sufficient. On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). -### Offline documentation (optional) +### Offline documentation -If you are going to be working on Rustlings while offline, you may want to run `rustup doc --std` to make the standard library documentation available on your machine. +Whenever you're working on Rustlings offline, you can access a local copy of the standard library documentation by running `rustup doc --std`. ## Usage From 870776d03bc612f473c8dbf1b19d3d78200ae3e7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 19 Apr 2026 00:37:10 +0200 Subject: [PATCH 56/78] Allow selecting next exercise with enter key --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index c60a5299..db466c2f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -83,7 +83,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('r') => list_state.reset_selected()?, - KeyCode::Char('c') => { + KeyCode::Char('c') | KeyCode::Enter => { if list_state.selected_to_current_exercise()? { return Ok(()); } From c658a997f3dde7e5d3d15126428e0546370ef0d4 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 20 Apr 2026 13:49:17 +0200 Subject: [PATCH 57/78] Turn unnecessary closure into regular function --- src/list/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 6e52eec1..c31cf8ae 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,7 +233,7 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; - let hotkey = |writer: &mut MaxLenWriter, hotkey| -> io::Result<()> { + fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { writer .stdout .queue(SetForegroundColor(Color::Yellow))? @@ -241,7 +241,7 @@ impl<'a> ListState<'a> { writer.write_ascii(hotkey)?; writer.stdout.queue(ResetColor)?; Ok(()) - }; + } let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { From b86a532e2848d71d103a5bc677eaf4b9d277ce54 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 13:53:25 +0200 Subject: [PATCH 58/78] Document enter keybind in list footer --- src/list/state.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 4fcbd3c3..e9386b8e 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -237,7 +237,8 @@ impl<'a> ListState<'a> { if self.message.is_empty() { // Help footer message if self.scroll_state.selected().is_some() { - writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; + writer + .write_str("↓/j ↑/k home/g end/G | ↩️/ontinue at | eset exercise")?; next_ln(stdout)?; writer = MaxLenWriter::new(stdout, self.term_width as usize); From e0334f79fe6c8ee522841154a48c34932b4346cf Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 14:04:29 +0200 Subject: [PATCH 59/78] Avoid nested function definition --- src/list/state.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index c31cf8ae..1bfdf58d 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -233,16 +233,6 @@ impl<'a> ListState<'a> { )?; next_ln(stdout)?; - fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { - writer - .stdout - .queue(SetForegroundColor(Color::Yellow))? - .queue(SetAttribute(Attribute::Bold))?; - writer.write_ascii(hotkey)?; - writer.stdout.queue(ResetColor)?; - Ok(()) - } - let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); if self.message.is_empty() { // Help footer message @@ -446,3 +436,14 @@ impl<'a> ListState<'a> { Ok(true) } } + +/// Draw an emphasized hotkey in the list footer. +fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { + writer + .stdout + .queue(SetForegroundColor(Color::Yellow))? + .queue(SetAttribute(Attribute::Bold))?; + writer.write_ascii(hotkey)?; + writer.stdout.queue(ResetColor)?; + Ok(()) +} From f9f8a37bc72bd0ac6562545e966e4d8cb26bc4d1 Mon Sep 17 00:00:00 2001 From: Piotr Spieker Date: Thu, 30 Apr 2026 12:05:38 +0200 Subject: [PATCH 60/78] Improve the list of enum variants in enums2 hint --- rustlings-macros/info.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index dd1f04be..735d131d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -440,7 +440,7 @@ dir = "08_enums" test = false hint = """ You can create enumerations that have different variants with different types -such as struct-like variants, regular structs, a single string, tuples, no data, etc.""" +such as struct-like, tuple-like and unit-only variants.""" [[exercises]] name = "enums3" From 124708acd93a4a48272e1f7a953fe62c9a2ffa09 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 3 May 2026 14:54:27 +0200 Subject: [PATCH 61/78] Use slice instead of array in iterator1 This avoids confusion between `.into_iter()` and `.iter()`. On a slice, both methods do the same (correct) thing. Using `.into_iter()` will result in a clippy warning about the slice not being consumed. --- exercises/18_iterators/iterators1.rs | 4 ++-- solutions/18_iterators/iterators1.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index ca937ed0..8014f0f1 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // TODO: Create an iterator over the array. + // TODO: Create an iterator over the slice. let mut fav_fruits_iterator = todo!(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs index 93a6008a..79977fa4 100644 --- a/solutions/18_iterators/iterators1.rs +++ b/solutions/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // Create an iterator over the array. + // Create an iterator over the slice. let mut fav_fruits_iterator = my_fav_fruits.iter(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); From d9e0b103c47edae04a7c0b2f8f541307a79afac2 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 9 May 2026 19:31:44 +0200 Subject: [PATCH 62/78] Update hint of enums2 This was discussed in another PR, but it was merged before the code was updated to the most recent consensus: https://github.com/rust-lang/rustlings/pull/2353 Co-authored-by: Piotr Spieker --- rustlings-macros/info.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index d88b549e..ae6e24a9 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -438,8 +438,14 @@ name = "enums2" dir = "08_enums" test = false hint = """ -You can create enumerations that have different variants with different types -such as struct-like, tuple-like and unit-only variants.""" +Enum variants can be defined using three different forms: struct-, tuple- and +unit-like. Here's an example enum definition, which uses all three forms: + +enum EnumUsingAllVariantForms { + StructLike { named_field: bool }, + TupleLike(bool), + UnitLike, +}""" [[exercises]] name = "enums3" From 848e3f929470d864d56f018d9f47daea5d258e66 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 Apr 2026 14:10:36 +0200 Subject: [PATCH 63/78] Upgrade upload-pages-artifact --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index d437cf03..135cd1e2 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -31,7 +31,7 @@ jobs: - name: Build site run: ./zola build - name: Upload static files as artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: website/public/ deploy: From 60edde0f590bf514bedff370115a52c7a728aa3b Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 17 Apr 2026 15:34:18 +0200 Subject: [PATCH 64/78] Make Ferris more symmetrical --- src/app_state.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 78b9c205..27c08b1d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -547,7 +547,7 @@ impl AppState { pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> { clear_terminal(stdout)?; - stdout.write_all(FENISH_LINE.as_bytes())?; + stdout.write_all(FINISH_LINE.as_bytes())?; let final_message = self.final_message.trim_ascii(); if !final_message.is_empty() { @@ -583,25 +583,25 @@ impl AppState { const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | +const FINISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the finish line! | +-------------------------- ------------------------+ - \\/\x1b[31m - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–‘β–‘β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ - β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ - β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–‘β–‘ β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m + \\/\x1b[31m + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ + β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ + β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m "; From a40a4dd43bd53f1419953c146cb1be5c17f7b868 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 17 Apr 2026 15:34:18 +0200 Subject: [PATCH 65/78] Update deps --- Cargo.lock | 76 ++++++++++++++++++++++++++-------------------------- src/watch.rs | 1 + 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8db4360..dfba4bcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,15 +60,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "cfg-if" @@ -78,9 +72,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -100,9 +94,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -128,7 +122,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "crossterm_winapi", "document-features", "mio", @@ -175,9 +169,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "foldhash" @@ -218,9 +212,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -236,12 +230,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -252,7 +246,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.11.0", + "bitflags", "inotify-sys", "libc", ] @@ -290,11 +284,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "285efcf12ef41bec907b3000d5ffaeb54191d4d9d83c0d6157e6cbc2db255e64" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", ] @@ -306,9 +300,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" @@ -361,7 +355,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", + "bitflags", "fsevent-sys", "inotify", "kqueue", @@ -379,7 +373,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -457,7 +451,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -466,7 +460,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -720,11 +714,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -733,7 +727,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -764,7 +758,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -892,9 +886,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -905,6 +899,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -954,7 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap", "log", "serde", diff --git a/src/watch.rs b/src/watch.rs index 857ccfde..f3804a40 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -150,6 +150,7 @@ pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result<()> { + // TODO: Use cfg_select! after bumping MSRV to at least 1.95 #[cfg(not(windows))] { let stdin_fd = rustix::stdio::stdin(); From e38c82ccbb92829204c1a9dd5e01baf09c6af1e2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 11 May 2026 12:13:53 +0200 Subject: [PATCH 66/78] Add a hint about opening the book offline --- website/content/setup/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/setup/index.md b/website/content/setup/index.md index 8147ff56..7595b628 100644 --- a/website/content/setup/index.md +++ b/website/content/setup/index.md @@ -14,7 +14,7 @@ This will also install _Cargo_, Rust's package/project manager. > > Debian: `sudo apt install gcc`\ > Fedora: `sudo dnf install gcc` - +> > 🍎 If you are on **MacOS**, make sure you have _Xcode and its developer tools_ installed: `xcode-select --install` ## Installing Rustlings @@ -75,7 +75,7 @@ On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). ### Offline documentation -Whenever you're working on Rustlings offline, you can access a local copy of the standard library documentation by running `rustup doc --std`. +Whenever you're working on Rustlings offline, you can access a local copy of the book or the standard library documentation by running `rustup doc --book` or `rustup doc --std`. ## Usage From 4338c5807952ffcba0ae6d1a3a75fd44a1201e3e Mon Sep 17 00:00:00 2001 From: Jane Date: Mon, 4 May 2026 22:55:51 +0800 Subject: [PATCH 67/78] Rename smart_pointers and conversions exercises with numeric prefix Update rustlings-macros/info.toml and dev/Cargo.toml accordingly --- dev/Cargo.toml | 36 +++++++++---------- .../19_smart_pointers/{box1.rs => 1_box1.rs} | 0 .../19_smart_pointers/{rc1.rs => 2_rc1.rs} | 0 .../19_smart_pointers/{arc1.rs => 3_arc1.rs} | 0 .../19_smart_pointers/{cow1.rs => 4_cow1.rs} | 0 .../{using_as.rs => 1_using_as.rs} | 0 .../{from_into.rs => 2_from_into.rs} | 0 .../{from_str.rs => 3_from_str.rs} | 0 .../{try_from_into.rs => 4_try_from_into.rs} | 0 .../{as_ref_mut.rs => 5_as_ref_mut.rs} | 0 rustlings-macros/info.toml | 18 +++++----- .../19_smart_pointers/{box1.rs => 1_box1.rs} | 0 .../19_smart_pointers/{rc1.rs => 2_rc1.rs} | 0 .../19_smart_pointers/{arc1.rs => 3_arc1.rs} | 0 .../19_smart_pointers/{cow1.rs => 4_cow1.rs} | 0 .../{using_as.rs => 1_using_as.rs} | 0 .../{from_into.rs => 2_from_into.rs} | 0 .../{from_str.rs => 3_from_str.rs} | 0 .../{try_from_into.rs => 4_try_from_into.rs} | 0 .../{as_ref_mut.rs => 5_as_ref_mut.rs} | 0 20 files changed, 27 insertions(+), 27 deletions(-) rename exercises/19_smart_pointers/{box1.rs => 1_box1.rs} (100%) rename exercises/19_smart_pointers/{rc1.rs => 2_rc1.rs} (100%) rename exercises/19_smart_pointers/{arc1.rs => 3_arc1.rs} (100%) rename exercises/19_smart_pointers/{cow1.rs => 4_cow1.rs} (100%) rename exercises/23_conversions/{using_as.rs => 1_using_as.rs} (100%) rename exercises/23_conversions/{from_into.rs => 2_from_into.rs} (100%) rename exercises/23_conversions/{from_str.rs => 3_from_str.rs} (100%) rename exercises/23_conversions/{try_from_into.rs => 4_try_from_into.rs} (100%) rename exercises/23_conversions/{as_ref_mut.rs => 5_as_ref_mut.rs} (100%) rename solutions/19_smart_pointers/{box1.rs => 1_box1.rs} (100%) rename solutions/19_smart_pointers/{rc1.rs => 2_rc1.rs} (100%) rename solutions/19_smart_pointers/{arc1.rs => 3_arc1.rs} (100%) rename solutions/19_smart_pointers/{cow1.rs => 4_cow1.rs} (100%) rename solutions/23_conversions/{using_as.rs => 1_using_as.rs} (100%) rename solutions/23_conversions/{from_into.rs => 2_from_into.rs} (100%) rename solutions/23_conversions/{from_str.rs => 3_from_str.rs} (100%) rename solutions/23_conversions/{try_from_into.rs => 4_try_from_into.rs} (100%) rename solutions/23_conversions/{as_ref_mut.rs => 5_as_ref_mut.rs} (100%) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 4f725b70..b531176a 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -150,14 +150,14 @@ bin = [ { name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" }, { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, { name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" }, - { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, - { name = "box1_sol", path = "../solutions/19_smart_pointers/box1.rs" }, - { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, - { name = "rc1_sol", path = "../solutions/19_smart_pointers/rc1.rs" }, - { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, - { name = "arc1_sol", path = "../solutions/19_smart_pointers/arc1.rs" }, - { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, - { name = "cow1_sol", path = "../solutions/19_smart_pointers/cow1.rs" }, + { name = "1_box1", path = "../exercises/19_smart_pointers/1_box1.rs" }, + { name = "1_box1_sol", path = "../solutions/19_smart_pointers/1_box1.rs" }, + { name = "2_rc1", path = "../exercises/19_smart_pointers/2_rc1.rs" }, + { name = "2_rc1_sol", path = "../solutions/19_smart_pointers/2_rc1.rs" }, + { name = "3_arc1", path = "../exercises/19_smart_pointers/3_arc1.rs" }, + { name = "3_arc1_sol", path = "../solutions/19_smart_pointers/3_arc1.rs" }, + { name = "4_cow1", path = "../exercises/19_smart_pointers/4_cow1.rs" }, + { name = "4_cow1_sol", path = "../solutions/19_smart_pointers/4_cow1.rs" }, { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, { name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" }, { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, @@ -178,16 +178,16 @@ bin = [ { name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" }, { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, { name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" }, - { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, - { name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" }, - { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, - { name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" }, - { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, - { name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" }, - { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, - { name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" }, - { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, - { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, + { name = "1_using_as", path = "../exercises/23_conversions/1_using_as.rs" }, + { name = "1_using_as_sol", path = "../solutions/23_conversions/1_using_as.rs" }, + { name = "2_from_into", path = "../exercises/23_conversions/2_from_into.rs" }, + { name = "2_from_into_sol", path = "../solutions/23_conversions/2_from_into.rs" }, + { name = "3_from_str", path = "../exercises/23_conversions/3_from_str.rs" }, + { name = "3_from_str_sol", path = "../solutions/23_conversions/3_from_str.rs" }, + { name = "4_try_from_into", path = "../exercises/23_conversions/4_try_from_into.rs" }, + { name = "4_try_from_into_sol", path = "../solutions/23_conversions/4_try_from_into.rs" }, + { name = "5_as_ref_mut", path = "../exercises/23_conversions/5_as_ref_mut.rs" }, + { name = "5_as_ref_mut_sol", path = "../solutions/23_conversions/5_as_ref_mut.rs" }, ] [package] diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/1_box1.rs similarity index 100% rename from exercises/19_smart_pointers/box1.rs rename to exercises/19_smart_pointers/1_box1.rs diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/2_rc1.rs similarity index 100% rename from exercises/19_smart_pointers/rc1.rs rename to exercises/19_smart_pointers/2_rc1.rs diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/3_arc1.rs similarity index 100% rename from exercises/19_smart_pointers/arc1.rs rename to exercises/19_smart_pointers/3_arc1.rs diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/4_cow1.rs similarity index 100% rename from exercises/19_smart_pointers/cow1.rs rename to exercises/19_smart_pointers/4_cow1.rs diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/1_using_as.rs similarity index 100% rename from exercises/23_conversions/using_as.rs rename to exercises/23_conversions/1_using_as.rs diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/2_from_into.rs similarity index 100% rename from exercises/23_conversions/from_into.rs rename to exercises/23_conversions/2_from_into.rs diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/3_from_str.rs similarity index 100% rename from exercises/23_conversions/from_str.rs rename to exercises/23_conversions/3_from_str.rs diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/4_try_from_into.rs similarity index 100% rename from exercises/23_conversions/try_from_into.rs rename to exercises/23_conversions/4_try_from_into.rs diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/5_as_ref_mut.rs similarity index 100% rename from exercises/23_conversions/as_ref_mut.rs rename to exercises/23_conversions/5_as_ref_mut.rs diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ae6e24a9..0dc6e9c6 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -959,7 +959,7 @@ a different method that could make your code more compact than using `fold`.""" # SMART POINTERS [[exercises]] -name = "box1" +name = "1_box1" dir = "19_smart_pointers" hint = """ The compiler's message should help: Since we cannot store the value of the @@ -976,7 +976,7 @@ Although the current list is one of integers (`i32`), feel free to change the definition and try other types!""" [[exercises]] -name = "rc1" +name = "2_rc1" dir = "19_smart_pointers" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has @@ -993,7 +993,7 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html Unfortunately, Pluto is no longer considered a planet :(""" [[exercises]] -name = "arc1" +name = "3_arc1" dir = "19_smart_pointers" test = false hint = """ @@ -1010,7 +1010,7 @@ Book: https://doc.rust-lang.org/book/ch16-00-concurrency.html""" [[exercises]] -name = "cow1" +name = "4_cow1" dir = "19_smart_pointers" hint = """ If `Cow` already owns the data, it doesn't need to clone it when `to_mut()` is @@ -1161,20 +1161,20 @@ hint = "No hints this time!" # TYPE CONVERSIONS [[exercises]] -name = "using_as" +name = "1_using_as" dir = "23_conversions" hint = """ Use the `as` operator to cast one of the operands in the last line of the `average` function into the expected return type.""" [[exercises]] -name = "from_into" +name = "2_from_into" dir = "23_conversions" hint = """ Follow the steps provided right before the `From` implementation.""" [[exercises]] -name = "from_str" +name = "3_from_str" dir = "23_conversions" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, @@ -1191,7 +1191,7 @@ operator in your solution, you might want to look at https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] -name = "try_from_into" +name = "4_try_from_into" dir = "23_conversions" hint = """ Is there an implementation of `TryFrom` in the standard library that can both do @@ -1201,7 +1201,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" [[exercises]] -name = "as_ref_mut" +name = "5_as_ref_mut" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/1_box1.rs similarity index 100% rename from solutions/19_smart_pointers/box1.rs rename to solutions/19_smart_pointers/1_box1.rs diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/2_rc1.rs similarity index 100% rename from solutions/19_smart_pointers/rc1.rs rename to solutions/19_smart_pointers/2_rc1.rs diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/3_arc1.rs similarity index 100% rename from solutions/19_smart_pointers/arc1.rs rename to solutions/19_smart_pointers/3_arc1.rs diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/4_cow1.rs similarity index 100% rename from solutions/19_smart_pointers/cow1.rs rename to solutions/19_smart_pointers/4_cow1.rs diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/1_using_as.rs similarity index 100% rename from solutions/23_conversions/using_as.rs rename to solutions/23_conversions/1_using_as.rs diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/2_from_into.rs similarity index 100% rename from solutions/23_conversions/from_into.rs rename to solutions/23_conversions/2_from_into.rs diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/3_from_str.rs similarity index 100% rename from solutions/23_conversions/from_str.rs rename to solutions/23_conversions/3_from_str.rs diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/4_try_from_into.rs similarity index 100% rename from solutions/23_conversions/try_from_into.rs rename to solutions/23_conversions/4_try_from_into.rs diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/5_as_ref_mut.rs similarity index 100% rename from solutions/23_conversions/as_ref_mut.rs rename to solutions/23_conversions/5_as_ref_mut.rs From 5b1edf5f4fa88f8d35a9c751e3900a2ae260ef24 Mon Sep 17 00:00:00 2001 From: Jane Date: Wed, 6 May 2026 18:04:01 +0800 Subject: [PATCH 68/78] Rename smart_pointers and conversions exercises with numeric prefix (Option A) --- dev/Cargo.toml | 36 +++++++++---------- .../{1_box1.rs => smart_pointers1.rs} | 0 .../{2_rc1.rs => smart_pointers2.rs} | 0 .../{3_arc1.rs => smart_pointers3.rs} | 0 .../{4_cow1.rs => smart_pointers4.rs} | 0 .../{1_using_as.rs => conversions1.rs} | 0 .../{2_from_into.rs => conversions2.rs} | 0 .../{3_from_str.rs => conversions3.rs} | 0 .../{4_try_from_into.rs => conversions4.rs} | 0 .../{5_as_ref_mut.rs => conversions5.rs} | 0 rustlings-macros/info.toml | 18 +++++----- .../{1_box1.rs => smart_pointers1.rs} | 0 .../{2_rc1.rs => smart_pointers2.rs} | 0 .../{3_arc1.rs => smart_pointers3.rs} | 0 .../{4_cow1.rs => smart_pointers4.rs} | 0 .../{1_using_as.rs => conversions1.rs} | 0 .../{2_from_into.rs => conversions2.rs} | 0 .../{3_from_str.rs => conversions3.rs} | 0 .../{4_try_from_into.rs => conversions4.rs} | 0 .../{5_as_ref_mut.rs => conversions5.rs} | 0 20 files changed, 27 insertions(+), 27 deletions(-) rename exercises/19_smart_pointers/{1_box1.rs => smart_pointers1.rs} (100%) rename exercises/19_smart_pointers/{2_rc1.rs => smart_pointers2.rs} (100%) rename exercises/19_smart_pointers/{3_arc1.rs => smart_pointers3.rs} (100%) rename exercises/19_smart_pointers/{4_cow1.rs => smart_pointers4.rs} (100%) rename exercises/23_conversions/{1_using_as.rs => conversions1.rs} (100%) rename exercises/23_conversions/{2_from_into.rs => conversions2.rs} (100%) rename exercises/23_conversions/{3_from_str.rs => conversions3.rs} (100%) rename exercises/23_conversions/{4_try_from_into.rs => conversions4.rs} (100%) rename exercises/23_conversions/{5_as_ref_mut.rs => conversions5.rs} (100%) rename solutions/19_smart_pointers/{1_box1.rs => smart_pointers1.rs} (100%) rename solutions/19_smart_pointers/{2_rc1.rs => smart_pointers2.rs} (100%) rename solutions/19_smart_pointers/{3_arc1.rs => smart_pointers3.rs} (100%) rename solutions/19_smart_pointers/{4_cow1.rs => smart_pointers4.rs} (100%) rename solutions/23_conversions/{1_using_as.rs => conversions1.rs} (100%) rename solutions/23_conversions/{2_from_into.rs => conversions2.rs} (100%) rename solutions/23_conversions/{3_from_str.rs => conversions3.rs} (100%) rename solutions/23_conversions/{4_try_from_into.rs => conversions4.rs} (100%) rename solutions/23_conversions/{5_as_ref_mut.rs => conversions5.rs} (100%) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index b531176a..66bc1dfe 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -150,14 +150,14 @@ bin = [ { name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" }, { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, { name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" }, - { name = "1_box1", path = "../exercises/19_smart_pointers/1_box1.rs" }, - { name = "1_box1_sol", path = "../solutions/19_smart_pointers/1_box1.rs" }, - { name = "2_rc1", path = "../exercises/19_smart_pointers/2_rc1.rs" }, - { name = "2_rc1_sol", path = "../solutions/19_smart_pointers/2_rc1.rs" }, - { name = "3_arc1", path = "../exercises/19_smart_pointers/3_arc1.rs" }, - { name = "3_arc1_sol", path = "../solutions/19_smart_pointers/3_arc1.rs" }, - { name = "4_cow1", path = "../exercises/19_smart_pointers/4_cow1.rs" }, - { name = "4_cow1_sol", path = "../solutions/19_smart_pointers/4_cow1.rs" }, + { name = "smart_pointers1", path = "../exercises/19_smart_pointers/smart_pointers1.rs" }, + { name = "smart_pointers1_sol", path = "../solutions/19_smart_pointers/smart_pointers1.rs" }, + { name = "smart_pointers2", path = "../exercises/19_smart_pointers/smart_pointers2.rs" }, + { name = "smart_pointers2_sol", path = "../solutions/19_smart_pointers/smart_pointers2.rs" }, + { name = "smart_pointers3", path = "../exercises/19_smart_pointers/smart_pointers3.rs" }, + { name = "smart_pointers3_sol", path = "../solutions/19_smart_pointers/smart_pointers3.rs" }, + { name = "smart_pointers4", path = "../exercises/19_smart_pointers/smart_pointers4.rs" }, + { name = "smart_pointers4_sol", path = "../solutions/19_smart_pointers/smart_pointers4.rs" }, { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, { name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" }, { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, @@ -178,16 +178,16 @@ bin = [ { name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" }, { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, { name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" }, - { name = "1_using_as", path = "../exercises/23_conversions/1_using_as.rs" }, - { name = "1_using_as_sol", path = "../solutions/23_conversions/1_using_as.rs" }, - { name = "2_from_into", path = "../exercises/23_conversions/2_from_into.rs" }, - { name = "2_from_into_sol", path = "../solutions/23_conversions/2_from_into.rs" }, - { name = "3_from_str", path = "../exercises/23_conversions/3_from_str.rs" }, - { name = "3_from_str_sol", path = "../solutions/23_conversions/3_from_str.rs" }, - { name = "4_try_from_into", path = "../exercises/23_conversions/4_try_from_into.rs" }, - { name = "4_try_from_into_sol", path = "../solutions/23_conversions/4_try_from_into.rs" }, - { name = "5_as_ref_mut", path = "../exercises/23_conversions/5_as_ref_mut.rs" }, - { name = "5_as_ref_mut_sol", path = "../solutions/23_conversions/5_as_ref_mut.rs" }, + { name = "conversions1", path = "../exercises/23_conversions/conversions1.rs" }, + { name = "conversions1_sol", path = "../solutions/23_conversions/conversions1.rs" }, + { name = "conversions2", path = "../exercises/23_conversions/conversions2.rs" }, + { name = "conversions2_sol", path = "../solutions/23_conversions/conversions2.rs" }, + { name = "conversions3", path = "../exercises/23_conversions/conversions3.rs" }, + { name = "conversions3_sol", path = "../solutions/23_conversions/conversions3.rs" }, + { name = "conversions4", path = "../exercises/23_conversions/conversions4.rs" }, + { name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" }, + { name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" }, + { name = "conversions5_sol", path = "../solutions/23_conversions/conversions5.rs" }, ] [package] diff --git a/exercises/19_smart_pointers/1_box1.rs b/exercises/19_smart_pointers/smart_pointers1.rs similarity index 100% rename from exercises/19_smart_pointers/1_box1.rs rename to exercises/19_smart_pointers/smart_pointers1.rs diff --git a/exercises/19_smart_pointers/2_rc1.rs b/exercises/19_smart_pointers/smart_pointers2.rs similarity index 100% rename from exercises/19_smart_pointers/2_rc1.rs rename to exercises/19_smart_pointers/smart_pointers2.rs diff --git a/exercises/19_smart_pointers/3_arc1.rs b/exercises/19_smart_pointers/smart_pointers3.rs similarity index 100% rename from exercises/19_smart_pointers/3_arc1.rs rename to exercises/19_smart_pointers/smart_pointers3.rs diff --git a/exercises/19_smart_pointers/4_cow1.rs b/exercises/19_smart_pointers/smart_pointers4.rs similarity index 100% rename from exercises/19_smart_pointers/4_cow1.rs rename to exercises/19_smart_pointers/smart_pointers4.rs diff --git a/exercises/23_conversions/1_using_as.rs b/exercises/23_conversions/conversions1.rs similarity index 100% rename from exercises/23_conversions/1_using_as.rs rename to exercises/23_conversions/conversions1.rs diff --git a/exercises/23_conversions/2_from_into.rs b/exercises/23_conversions/conversions2.rs similarity index 100% rename from exercises/23_conversions/2_from_into.rs rename to exercises/23_conversions/conversions2.rs diff --git a/exercises/23_conversions/3_from_str.rs b/exercises/23_conversions/conversions3.rs similarity index 100% rename from exercises/23_conversions/3_from_str.rs rename to exercises/23_conversions/conversions3.rs diff --git a/exercises/23_conversions/4_try_from_into.rs b/exercises/23_conversions/conversions4.rs similarity index 100% rename from exercises/23_conversions/4_try_from_into.rs rename to exercises/23_conversions/conversions4.rs diff --git a/exercises/23_conversions/5_as_ref_mut.rs b/exercises/23_conversions/conversions5.rs similarity index 100% rename from exercises/23_conversions/5_as_ref_mut.rs rename to exercises/23_conversions/conversions5.rs diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 0dc6e9c6..637283d2 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -959,7 +959,7 @@ a different method that could make your code more compact than using `fold`.""" # SMART POINTERS [[exercises]] -name = "1_box1" +name = "smart_pointers1" dir = "19_smart_pointers" hint = """ The compiler's message should help: Since we cannot store the value of the @@ -976,7 +976,7 @@ Although the current list is one of integers (`i32`), feel free to change the definition and try other types!""" [[exercises]] -name = "2_rc1" +name = "smart_pointers2" dir = "19_smart_pointers" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has @@ -993,7 +993,7 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html Unfortunately, Pluto is no longer considered a planet :(""" [[exercises]] -name = "3_arc1" +name = "smart_pointers3" dir = "19_smart_pointers" test = false hint = """ @@ -1010,7 +1010,7 @@ Book: https://doc.rust-lang.org/book/ch16-00-concurrency.html""" [[exercises]] -name = "4_cow1" +name = "smart_pointers4" dir = "19_smart_pointers" hint = """ If `Cow` already owns the data, it doesn't need to clone it when `to_mut()` is @@ -1161,20 +1161,20 @@ hint = "No hints this time!" # TYPE CONVERSIONS [[exercises]] -name = "1_using_as" +name = "conversions1" dir = "23_conversions" hint = """ Use the `as` operator to cast one of the operands in the last line of the `average` function into the expected return type.""" [[exercises]] -name = "2_from_into" +name = "conversions2" dir = "23_conversions" hint = """ Follow the steps provided right before the `From` implementation.""" [[exercises]] -name = "3_from_str" +name = "conversions3" dir = "23_conversions" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, @@ -1191,7 +1191,7 @@ operator in your solution, you might want to look at https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] -name = "4_try_from_into" +name = "conversions4" dir = "23_conversions" hint = """ Is there an implementation of `TryFrom` in the standard library that can both do @@ -1201,7 +1201,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" [[exercises]] -name = "5_as_ref_mut" +name = "conversions5" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/solutions/19_smart_pointers/1_box1.rs b/solutions/19_smart_pointers/smart_pointers1.rs similarity index 100% rename from solutions/19_smart_pointers/1_box1.rs rename to solutions/19_smart_pointers/smart_pointers1.rs diff --git a/solutions/19_smart_pointers/2_rc1.rs b/solutions/19_smart_pointers/smart_pointers2.rs similarity index 100% rename from solutions/19_smart_pointers/2_rc1.rs rename to solutions/19_smart_pointers/smart_pointers2.rs diff --git a/solutions/19_smart_pointers/3_arc1.rs b/solutions/19_smart_pointers/smart_pointers3.rs similarity index 100% rename from solutions/19_smart_pointers/3_arc1.rs rename to solutions/19_smart_pointers/smart_pointers3.rs diff --git a/solutions/19_smart_pointers/4_cow1.rs b/solutions/19_smart_pointers/smart_pointers4.rs similarity index 100% rename from solutions/19_smart_pointers/4_cow1.rs rename to solutions/19_smart_pointers/smart_pointers4.rs diff --git a/solutions/23_conversions/1_using_as.rs b/solutions/23_conversions/conversions1.rs similarity index 100% rename from solutions/23_conversions/1_using_as.rs rename to solutions/23_conversions/conversions1.rs diff --git a/solutions/23_conversions/2_from_into.rs b/solutions/23_conversions/conversions2.rs similarity index 100% rename from solutions/23_conversions/2_from_into.rs rename to solutions/23_conversions/conversions2.rs diff --git a/solutions/23_conversions/3_from_str.rs b/solutions/23_conversions/conversions3.rs similarity index 100% rename from solutions/23_conversions/3_from_str.rs rename to solutions/23_conversions/conversions3.rs diff --git a/solutions/23_conversions/4_try_from_into.rs b/solutions/23_conversions/conversions4.rs similarity index 100% rename from solutions/23_conversions/4_try_from_into.rs rename to solutions/23_conversions/conversions4.rs diff --git a/solutions/23_conversions/5_as_ref_mut.rs b/solutions/23_conversions/conversions5.rs similarity index 100% rename from solutions/23_conversions/5_as_ref_mut.rs rename to solutions/23_conversions/conversions5.rs From ef99b5cb9e4e0de56dd3437162c707a036c46365 Mon Sep 17 00:00:00 2001 From: Jane Date: Wed, 13 May 2026 19:57:17 +0800 Subject: [PATCH 69/78] docs: update README after rebase --- exercises/23_conversions/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/23_conversions/README.md b/exercises/23_conversions/README.md index 619a78c5..fe337331 100644 --- a/exercises/23_conversions/README.md +++ b/exercises/23_conversions/README.md @@ -2,14 +2,14 @@ Rust offers a multitude of ways to convert a value of a given type into another type. -The simplest form of type conversion is a type cast expression. It is denoted with the binary operator `as`. For instance, `println!("{}", 1 + 1.0);` would not compile, since `1` is an integer while `1.0` is a float. However, `println!("{}", 1 as f32 + 1.0)` should compile. The exercise [`using_as`](using_as.rs) tries to cover this. +The simplest form of type conversion is a type cast expression. It is denoted with the binary operator `as`. For instance, `println!("{}", 1 + 1.0);` would not compile, since `1` is an integer while `1.0` is a float. However, `println!("{}", 1 as f32 + 1.0)` should compile. The exercise [`conversions1`](conversions1.rs) tries to cover this. Rust also offers traits that facilitate type conversions upon implementation. These traits can be found under the [`convert`](https://doc.rust-lang.org/std/convert/index.html) module. The traits are the following: -- `From` and `Into` covered in [`from_into`](from_into.rs) -- `TryFrom` and `TryInto` covered in [`try_from_into`](try_from_into.rs) -- `AsRef` and `AsMut` covered in [`as_ref_mut`](as_ref_mut.rs) +- `From` and `Into` covered in [`conversions2`](conversions2.rs) +- `TryFrom` and `TryInto` covered in [`conversions4`](conversions4.rs) +- `AsRef` and `AsMut` covered in [`conversions5`](conversions5.rs) Furthermore, the `std::str` module offers a trait called [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) which helps with converting strings into target types via the `parse` method on strings. If properly implemented for a given type `Person`, then `let p: Person = "Mark,20".parse().unwrap()` should both compile and run without panicking. From 03ddf3683b8d1108d55b612fd006dfb79ed43421 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 14 May 2026 16:08:29 +0200 Subject: [PATCH 70/78] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15818a9..d331ecd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Avoid initializing a nested Git repository [@senekor](https://github.com/senekor) - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. - `structs3`: Rewrote the exercise to make users type method syntax themselves. +- Rename the exercises for smart pointers and conversions so they're sorted alphabetically. [@foxfromworld](https://github.com/foxfromworld) ## 6.5.0 (2025-08-21) From 97a723508c260ea27aca9be8b3f6db3b3bed80f3 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 14 May 2026 15:50:18 +0200 Subject: [PATCH 71/78] Redesign vec1 to avoid confusing array literal Learners were sometimes confused by the literal array. Their assumption was that they are supposed to convert the array to a vector. Duplicating the literal elements was not an intuitive solution. This redesign avoids putting an identical array literal near the place where learners are supposed to use the vec! macro. --- CHANGELOG.md | 1 + exercises/05_vecs/vecs1.rs | 20 +++++++++----------- solutions/05_vecs/vecs1.rs | 18 ++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d331ecd4..e2acf6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - `vecs2`: Removed the use of `map` and `collect`, which are only taught later. - `structs3`: Rewrote the exercise to make users type method syntax themselves. - Rename the exercises for smart pointers and conversions so they're sorted alphabetically. [@foxfromworld](https://github.com/foxfromworld) +- `vecs1`: Remove array literal. Some learners assumed their task is to convert it to a vector. ## 6.5.0 (2025-08-21) diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 68e1affa..ab2e5ca5 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,11 +1,6 @@ -fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // Array - - // TODO: Create a vector called `v` which contains the exact same elements as in the array `a`. - // Use the vector macro. - // let v = ???; - - (a, v) +fn elems_to_vec(a: i32, b: i32, c: i32) -> Vec { + // TODO: Return a vector containing the elements a, b and c. + // Use the "vec!" macro. } fn main() { @@ -17,8 +12,11 @@ mod tests { use super::*; #[test] - fn test_array_and_vec_similarity() { - let (a, v) = array_and_vec(); - assert_eq!(a, *v); + fn test_elems_to_vec() { + let (a, b, c) = (2, 7, 12); + let v = elems_to_vec(a, b, c); + assert_eq!(v[0], a); + assert_eq!(v[1], b); + assert_eq!(v[2], c); } } diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs index 55b5676c..f79f4ebf 100644 --- a/solutions/05_vecs/vecs1.rs +++ b/solutions/05_vecs/vecs1.rs @@ -1,10 +1,5 @@ -fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // Array - - // Used the `vec!` macro. - let v = vec![10, 20, 30, 40]; - - (a, v) +fn elems_to_vec(a: i32, b: i32, c: i32) -> Vec { + vec![a, b, c] } fn main() { @@ -16,8 +11,11 @@ mod tests { use super::*; #[test] - fn test_array_and_vec_similarity() { - let (a, v) = array_and_vec(); - assert_eq!(a, *v); + fn test_elems_to_vec() { + let (a, b, c) = (2, 7, 12); + let v = elems_to_vec(a, b, c); + assert_eq!(v[0], a); + assert_eq!(v[1], b); + assert_eq!(v[2], c); } } From f3036315a054a5eccc98de412735821316795ade Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 14 May 2026 23:31:11 +0200 Subject: [PATCH 72/78] Clarify order of elements Co-authored-by: Mo Bitar <76752051+mo8it@users.noreply.github.com> --- exercises/05_vecs/vecs1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index ab2e5ca5..a015c40f 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,5 +1,5 @@ fn elems_to_vec(a: i32, b: i32, c: i32) -> Vec { - // TODO: Return a vector containing the elements a, b and c. + // TODO: Return a vector containing the elements a, b and c (in this order). // Use the "vec!" macro. } From db5ad7f42f8b59dc8b2c7cf6ff26203d447a6f73 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 3 May 2026 13:59:32 +0200 Subject: [PATCH 73/78] Use infallible conversion to teach From trait --- CHANGELOG.md | 1 + exercises/23_conversions/conversions2.rs | 134 +++++----------------- rustlings-macros/info.toml | 4 +- solutions/23_conversions/conversions2.rs | 138 +++++------------------ 4 files changed, 60 insertions(+), 217 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2acf6c6..807f7d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - `structs3`: Rewrote the exercise to make users type method syntax themselves. - Rename the exercises for smart pointers and conversions so they're sorted alphabetically. [@foxfromworld](https://github.com/foxfromworld) - `vecs1`: Remove array literal. Some learners assumed their task is to convert it to a vector. +- `conversions2`: Redesign the context such that infallible conversion makes sense. ## 6.5.0 (2025-08-21) diff --git a/exercises/23_conversions/conversions2.rs b/exercises/23_conversions/conversions2.rs index bc2783a3..f6df67f2 100644 --- a/exercises/23_conversions/conversions2.rs +++ b/exercises/23_conversions/conversions2.rs @@ -2,49 +2,36 @@ // implemented, an implementation of `Into` is automatically provided. // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html +// +// Frank the fairy would like to buy some truffles from Grace the gnome, a +// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and +// Frank only has FairyCredit. Help Frank by providing a `From` implementation +// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one +// FairyCredit is valued at 100 GnomeCoin. #[derive(Debug)] -struct Person { - name: String, - age: u8, +struct FairyCredit(u32); + +#[derive(Debug, PartialEq)] +struct GnomeCoin(u64); + +impl From for GnomeCoin { + // TODO: implement From for GnomeCoin } -// We implement the Default trait to use it as a fallback when the provided -// string is not convertible into a `Person` object. -impl Default for Person { - fn default() -> Self { - Self { - name: String::from("John"), - age: 30, - } - } -} - -// TODO: Complete this `From` implementation to be able to parse a `Person` -// out of a string in the form of "Mark,20". -// Note that you'll need to parse the age component into a `u8` with something -// like `"4".parse::()`. -// -// Steps: -// 1. Split the given string on the commas present in it. -// 2. If the split operation returns less or more than 2 elements, return the -// default of `Person`. -// 3. Use the first element from the split operation as the name. -// 4. If the name is empty, return the default of `Person`. -// 5. Parse the second element from the split operation into a `u8` as the age. -// 6. If parsing the age fails, return the default of `Person`. -impl From<&str> for Person { - fn from(s: &str) -> Self {} -} +// Note that we shouldn't provide the opposite conversion: from GnomeCoin to +// FairyCredits. That's because less than 100 GnomeCoins cannot be represented +// as FairyCredits, which would make the conversion lossy. The `From` trait is +// only appropriate for infallible and lossless conversions. fn main() { // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); + let g1 = GnomeCoin::from(FairyCredit(12)); + println!("{g1:?}"); - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); + // Since `From` is implemented for GnomeCoin, we are able to use `Into`. + let g2: GnomeCoin = FairyCredit(9).into(); + println!("{g2:?}"); } #[cfg(test)] @@ -52,79 +39,14 @@ mod tests { use super::*; #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); + fn test_from() { + let g = GnomeCoin::from(FairyCredit(12)); + assert_eq!(g, GnomeCoin(1200)); } #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_age() { - let p = Person::from("Mark,twenty"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_comma_and_age() { - let p: Person = Person::from("Mark"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_age() { - let p: Person = Person::from("Mark,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name() { - let p: Person = Person::from(",1"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_age() { - let p: Person = Person::from(","); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_invalid_age() { - let p: Person = Person::from(",one"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma() { - let p: Person = Person::from("Mike,32,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma_and_some_string() { - let p: Person = Person::from("Mike,32,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); + fn test_into() { + let g: GnomeCoin = FairyCredit(9).into(); + assert_eq!(g, GnomeCoin(900)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 637283d2..054da99b 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,7 +1171,9 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -Follow the steps provided right before the `From` implementation.""" +Implement From for GnomeCoin. Check the documentation of `From` to +learn about its required items: +https://doc.rust-lang.org/std/convert/trait.From.html""" [[exercises]] name = "conversions3" diff --git a/solutions/23_conversions/conversions2.rs b/solutions/23_conversions/conversions2.rs index cec23cb4..b8b2372e 100644 --- a/solutions/23_conversions/conversions2.rs +++ b/solutions/23_conversions/conversions2.rs @@ -2,55 +2,38 @@ // implemented, an implementation of `Into` is automatically provided. // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html +// +// Frank the fairy would like to buy some truffles from Grace the gnome, a +// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and +// Frank only has FairyCredit. Help Frank by providing a `From` implementation +// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one +// FairyCredit is valued at 100 GnomeCoin. #[derive(Debug)] -struct Person { - name: String, - age: u8, -} +struct FairyCredit(u32); -// We implement the Default trait to use it as a fallback when the provided -// string is not convertible into a `Person` object. -impl Default for Person { - fn default() -> Self { - Self { - name: String::from("John"), - age: 30, - } +#[derive(Debug, PartialEq)] +struct GnomeCoin(u64); + +impl From for GnomeCoin { + fn from(value: FairyCredit) -> Self { + Self(value.0 as u64 * 100) } } -impl From<&str> for Person { - fn from(s: &str) -> Self { - let mut split = s.split(','); - let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { - // ^^^^ there should be no third element - return Self::default(); - }; - - if name.is_empty() { - return Self::default(); - } - - let Ok(age) = age.parse() else { - return Self::default(); - }; - - Self { - name: name.into(), - age, - } - } -} +// Note that we shouldn't provide the opposite conversion: from GnomeCoin to +// FairyCredits. That's because less than 100 GnomeCoins cannot be represented +// as FairyCredits, which would make the conversion lossy. The `From` trait is +// only appropriate for infallible and lossless conversions. fn main() { // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); + let g1 = GnomeCoin::from(FairyCredit(12)); + println!("{g1:?}"); - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); + // Since `From` is implemented for GnomeCoin, we are able to use `Into`. + let g2: GnomeCoin = FairyCredit(9).into(); + println!("{g2:?}"); } #[cfg(test)] @@ -58,79 +41,14 @@ mod tests { use super::*; #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); + fn test_from() { + let g = GnomeCoin::from(FairyCredit(12)); + assert_eq!(g, GnomeCoin(1200)); } #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_age() { - let p = Person::from("Mark,twenty"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_comma_and_age() { - let p: Person = Person::from("Mark"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_age() { - let p: Person = Person::from("Mark,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name() { - let p: Person = Person::from(",1"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_age() { - let p: Person = Person::from(","); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_invalid_age() { - let p: Person = Person::from(",one"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma() { - let p: Person = Person::from("Mike,32,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma_and_some_string() { - let p: Person = Person::from("Mike,32,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); + fn test_into() { + let g: GnomeCoin = FairyCredit(9).into(); + assert_eq!(g, GnomeCoin(900)); } } From 360344ab6c5323e4289589ca0c1b6c2d23927b47 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 15 May 2026 01:42:30 +0200 Subject: [PATCH 74/78] Simplify story and increase difficulty Conversion between Celsius and Fahrenheit should be understandable to most. Inverting the formula is still not very hard, but a little harder than only multiplying by 100. --- exercises/23_conversions/conversions2.rs | 58 +++++++++++----------- rustlings-macros/info.toml | 5 +- solutions/23_conversions/conversions2.rs | 61 +++++++++++++----------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/exercises/23_conversions/conversions2.rs b/exercises/23_conversions/conversions2.rs index f6df67f2..491b8073 100644 --- a/exercises/23_conversions/conversions2.rs +++ b/exercises/23_conversions/conversions2.rs @@ -3,50 +3,52 @@ // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html // -// Frank the fairy would like to buy some truffles from Grace the gnome, a -// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and -// Frank only has FairyCredit. Help Frank by providing a `From` implementation -// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one -// FairyCredit is valued at 100 GnomeCoin. +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. -#[derive(Debug)] -struct FairyCredit(u32); +struct Celsius(f64); -#[derive(Debug, PartialEq)] -struct GnomeCoin(u64); +struct Fahrenheit(f64); -impl From for GnomeCoin { - // TODO: implement From for GnomeCoin +impl From for Fahrenheit { + // TODO: Convert Celsius to Fahrenheit. Don't worry about floating-point + // precision. The formula is: F = C * 1.8 + 32 } -// Note that we shouldn't provide the opposite conversion: from GnomeCoin to -// FairyCredits. That's because less than 100 GnomeCoins cannot be represented -// as FairyCredits, which would make the conversion lossy. The `From` trait is -// only appropriate for infallible and lossless conversions. +impl From for Celsius { + // TODO: Convert Fahrenheit to Celsius. +} fn main() { - // Use the `from` function. - let g1 = GnomeCoin::from(FairyCredit(12)); - println!("{g1:?}"); - - // Since `From` is implemented for GnomeCoin, we are able to use `Into`. - let g2: GnomeCoin = FairyCredit(9).into(); - println!("{g2:?}"); + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + #[test] - fn test_from() { - let g = GnomeCoin::from(FairyCredit(12)); - assert_eq!(g, GnomeCoin(1200)); + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } } #[test] - fn test_into() { - let g: GnomeCoin = FairyCredit(9).into(); - assert_eq!(g, GnomeCoin(900)); + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 054da99b..08f83e5f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,9 +1171,8 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -Implement From for GnomeCoin. Check the documentation of `From` to -learn about its required items: -https://doc.rust-lang.org/std/convert/trait.From.html""" +For the conversion from Fahrenheit to Celsius, you have to determine the formula +yourself. Don't forget the order of operations!""" [[exercises]] name = "conversions3" diff --git a/solutions/23_conversions/conversions2.rs b/solutions/23_conversions/conversions2.rs index b8b2372e..c8580f6b 100644 --- a/solutions/23_conversions/conversions2.rs +++ b/solutions/23_conversions/conversions2.rs @@ -3,52 +3,55 @@ // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html // -// Frank the fairy would like to buy some truffles from Grace the gnome, a -// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and -// Frank only has FairyCredit. Help Frank by providing a `From` implementation -// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one -// FairyCredit is valued at 100 GnomeCoin. +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. -#[derive(Debug)] -struct FairyCredit(u32); +struct Celsius(f64); -#[derive(Debug, PartialEq)] -struct GnomeCoin(u64); +struct Fahrenheit(f64); -impl From for GnomeCoin { - fn from(value: FairyCredit) -> Self { - Self(value.0 as u64 * 100) +impl From for Fahrenheit { + fn from(Celsius(celsius): Celsius) -> Self { + Fahrenheit(celsius * 1.8 + 32.0) } } -// Note that we shouldn't provide the opposite conversion: from GnomeCoin to -// FairyCredits. That's because less than 100 GnomeCoins cannot be represented -// as FairyCredits, which would make the conversion lossy. The `From` trait is -// only appropriate for infallible and lossless conversions. +impl From for Celsius { + fn from(Fahrenheit(fahrenheit): Fahrenheit) -> Self { + Celsius((fahrenheit - 32.0) / 1.8) + } +} fn main() { - // Use the `from` function. - let g1 = GnomeCoin::from(FairyCredit(12)); - println!("{g1:?}"); - - // Since `From` is implemented for GnomeCoin, we are able to use `Into`. - let g2: GnomeCoin = FairyCredit(9).into(); - println!("{g2:?}"); + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + #[test] - fn test_from() { - let g = GnomeCoin::from(FairyCredit(12)); - assert_eq!(g, GnomeCoin(1200)); + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } } #[test] - fn test_into() { - let g: GnomeCoin = FairyCredit(9).into(); - assert_eq!(g, GnomeCoin(900)); + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } } } From 361a7f501e0b8b40d0eac819b5efeb6f2667574f Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 15 May 2026 10:33:41 +0200 Subject: [PATCH 75/78] Add inverted formula and its derivation to hint --- rustlings-macros/info.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 08f83e5f..6b6af3a1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,8 +1171,13 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -For the conversion from Fahrenheit to Celsius, you have to determine the formula -yourself. Don't forget the order of operations!""" +The formula for converting from Fahrenheit to Celsius is: C = (F - 32) / 1.8 +This can be derived from the first formula: + + F = C * 1.8 + 32 // now subtract 32 on both sides + F - 32 = C * 1.8 // then divide by 1.8 +(F - 32) / 1.8 = C +""" [[exercises]] name = "conversions3" From 60b369a2fdbae2a664cb54283bf8b1407f141c91 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 17 May 2026 08:47:27 +0200 Subject: [PATCH 76/78] Explain changed line in if3 solution --- solutions/03_if/if3.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs index 571644d4..9405f085 100644 --- a/solutions/03_if/if3.rs +++ b/solutions/03_if/if3.rs @@ -2,6 +2,7 @@ fn animal_habitat(animal: &str) -> &str { let identifier = if animal == "crab" { 1 } else if animal == "gopher" { + // Integer, so that every branch has the same type. 2 } else if animal == "snake" { 3 From b18a8c3036c962d3a09bd04d0537ad86c046b3f7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Tue, 19 May 2026 17:29:16 +0200 Subject: [PATCH 77/78] strings4: remove From-based conversion To understand From-based conversion, an understanding of traits is required, which we teach in a later chapter. The From trait specifically is taught in one of the conversion exercises. So, we can safely remove it here without users missing out on learning something important. A specific source of confusion for users was a warning that the conversion is useless, which appeared when using the `string_slice` function for the expression with `.into()`. closes #2190 --- exercises/09_strings/strings4.rs | 2 -- solutions/09_strings/strings4.rs | 9 --------- 2 files changed, 11 deletions(-) diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index 47307263..43976a64 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -21,8 +21,6 @@ fn main() { placeholder("rust is fun!".to_owned()); - placeholder("nice weather".into()); - placeholder(format!("Interpolation {}", "Station")); // WARNING: This is byte indexing, not character indexing. diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 087b0386..ab976d27 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -15,15 +15,6 @@ fn main() { string("rust is fun!".to_owned()); - // Here, both answers work. - // `.into()` converts a type into an expected type. - // If it is called where `String` is expected, it will convert `&str` to `String`. - string("nice weather".into()); - // But if it is called where `&str` is expected, then `&str` is kept as `&str` since no conversion is needed. - // If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion. - #[allow(clippy::useless_conversion)] - string_slice("nice weather".into()); - string(format!("Interpolation {}", "Station")); // WARNING: This is byte indexing, not character indexing. From 9172a5bf276dd27a660a852a023358b20df59c61 Mon Sep 17 00:00:00 2001 From: Zhijie Wang Date: Sun, 24 May 2026 16:51:48 -0700 Subject: [PATCH 78/78] Align section references with Rust Book section names --- rustlings-macros/info.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 6b6af3a1..a4c72c12 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -265,10 +265,10 @@ for `a.len() >= 100`?""" name = "primitive_types4" dir = "04_primitive_types" hint = """ -Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section -of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the -starting and ending (plus one) indices of the items in the array that you want -to end up in the slice. +Take a look at the 'Understanding Ownership -> The Slice Type -> Other Slices' +section of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use +the starting and ending (plus one) indices of the items in the array that you +want to end up in the slice. If you're curious why the first argument of `assert_eq!` does not have an ampersand for a reference since the second argument is a reference, take a look