Compare commits

..

No commits in common. "main" and "v6.2.0" have entirely different histories.
main ... v6.2.0

125 changed files with 2316 additions and 3584 deletions

View File

@ -1,2 +0,0 @@
[alias]
dev = ["run", "--", "dev"]

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*.rs]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4

View File

@ -1,16 +1,10 @@
name: Check name: Rustlings Tests
on: on:
push: push:
branches: [main] branches: [main]
paths-ignore:
- website
- '*.md'
pull_request: pull_request:
branches: [main] branches: [main]
paths-ignore:
- website
- '*.md'
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -20,28 +14,30 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Clippy - run: cargo clippy -- --deny warnings
run: cargo clippy -- --deny warnings
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: rustfmt - uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: "exercises/**/*.md"
- name: Run cargo fmt
run: cargo fmt --all --check run: cargo fmt --all --check
test: test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
- name: cargo test - name: Run cargo test
run: cargo test --workspace run: cargo test --workspace
dev-check: dev-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2
- name: rustlings dev check - name: Run rustlings dev check
run: cargo dev check --require-solutions run: cargo run -- dev check --require-solutions

87
.github/workflows/web.yml vendored Normal file
View File

@ -0,0 +1,87 @@
# Workflow to build your docs with oranda (and mdbook)
# and deploy them to Github Pages
name: Web
# We're going to push to the gh-pages branch, so we need that permission
permissions:
contents: write
# What situations do we want to build docs in?
# All of these work independently and can be removed / commented out
# if you don't want oranda/mdbook running in that situation
on:
# Check that a PR didn't break docs!
#
# Note that the "Deploy to Github Pages" step won't run in this mode,
# so this won't have any side-effects. But it will tell you if a PR
# completely broke oranda/mdbook. Sadly we don't provide previews (yet)!
pull_request:
# Whenever something gets pushed to main, update the docs!
# This is great for getting docs changes live without cutting a full release.
#
# Note that if you're using cargo-dist, this will "race" the Release workflow
# that actually builds the Github Release that oranda tries to read (and
# this will almost certainly complete first). As a result you will publish
# docs for the latest commit but the oranda landing page won't know about
# the latest release. The workflow_run trigger below will properly wait for
# cargo-dist, and so this half-published state will only last for ~10 minutes.
#
# If you only want docs to update with releases, disable this, or change it to
# a "release" branch. You can, of course, also manually trigger a workflow run
# when you want the docs to update.
push:
branches:
- main
# Whenever a workflow called "Release" completes, update the docs!
#
# If you're using cargo-dist, this is recommended, as it will ensure that
# oranda always sees the latest release right when it's available. Note
# however that Github's UI is wonky when you use workflow_run, and won't
# show this workflow as part of any commit. You have to go to the "actions"
# tab for your repo to see this one running (the gh-pages deploy will also
# only show up there).
workflow_run:
workflows: [ "Release" ]
types:
- completed
# Alright, let's do it!
jobs:
web:
name: Build and deploy site and docs
runs-on: ubuntu-latest
steps:
# Setup
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: swatinem/rust-cache@v2
# If you use any mdbook plugins, here's the place to install them!
# Install and run oranda (and mdbook)
# This will write all output to ./public/ (including copying mdbook's output to there)
- name: Install and run oranda
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.3.1/oranda-installer.sh | sh
oranda build
# Deploy to our gh-pages branch (creating it if it doesn't exist)
# the "public" dir that oranda made above will become the root dir
# of this branch.
#
# Note that once the gh-pages branch exists, you must
# go into repo's settings > pages and set "deploy from branch: gh-pages"
# the other defaults work fine.
- name: Deploy to Github Pages
uses: JamesIves/github-pages-deploy-action@v4.4.1
# ONLY if we're on main (so no PRs or feature branches allowed!)
if: ${{ github.ref == 'refs/heads/main' }}
with:
branch: gh-pages
# Gotta tell the action where to find oranda's output
folder: public
token: ${{ secrets.GITHUB_TOKEN }}
single-commit: true

View File

@ -1,43 +0,0 @@
name: Website
on:
workflow_dispatch:
push:
branches: [main]
paths: [website]
jobs:
build:
defaults:
run:
working-directory: website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install TailwindCSS
run: npm install
- 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
- name: Build site
run: ./zola build
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v3
with:
path: website/public/
deploy:
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4

4
.gitignore vendored
View File

@ -6,6 +6,10 @@ Cargo.lock
# State file # State file
.rustlings-state.txt .rustlings-state.txt
# oranda
public/
.netlify
# OS # OS
.DS_Store .DS_Store
.direnv/ .direnv/

2
.markdownlint.yml Normal file
View File

@ -0,0 +1,2 @@
# MD013/line-length Line length, Expected: 80
MD013: false

View File

@ -1,7 +1,7 @@
[default.extend-words]
"earch" = "earch" # Because of <s>earch in the list footer
[files] [files]
extend-exclude = [ extend-exclude = [
"CHANGELOG.md", "CHANGELOG.md",
] ]
[default.extend-words]
"ratatui" = "ratatui"

View File

@ -1,95 +1,4 @@
## Unreleased <a name="6.2.0"></a>
### Changed
- `vecs2`: Removed the use of `map` and `collect`, which are only taught later.
## 6.5.0 (2025-08-21)
### Added
- Check that Clippy is installed before initialization
### Changed
- Upgrade to Rust edition 2024
- Raise the minimum supported Rust version to `1.88`
- Don't follow symlinks in the file watcher
- `dev new`: Don't add `.rustlings-state.txt` to `.gitignore`
### Fixed
- Fix file links in VS Code
- Fix error printing when the progress bar is shown
- `dev check`: Don't check formatting if there are no solution files
## 6.4.0 (2024-11-11)
### Added
- The list of exercises is now searchable by pressing `s` or `/` 🔍️ (thanks to [@frroossst](https://github.com/frroossst))
- New option `c` in the prompt to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- New command `check-all` to manually check all exercises ✅ (thanks to [@Nahor](https://github.com/Nahor))
- Addictive animation for showing the progress of checking all exercises. A nice showcase of parallelism in Rust ✨
- New option `x` in the prompt to reset the file of the current exercise 🔄
- Allow `dead_code` for all exercises and solutions ⚰️ (thanks to [@huss4in](https://github.com/huss4in))
- Pause input while running an exercise to avoid unexpected prompt interactions ⏸️
- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? 🔝
### Changed
- `enums3`: Remove redundant enum definition task (thanks to [@senekor](https://github.com/senekor))
- `if2`: Make the exercise less confusing by avoiding "fizz", "fuzz", "foo", "bar" and "baz" (thanks to [@senekor](https://github.com/senekor))
- `hashmap3`: Use the method `Entry::or_default`.
- Update the state of all exercises when checking all of them (thanks to [@Nahor](https://github.com/Nahor))
- The main prompt doesn't need a confirmation with ENTER on Unix-like systems anymore.
- No more jumping back to a previous exercise when its file is changed. Use the list to jump between exercises.
- Dump the solution file after an exercise is done even if the solution's directory doesn't exist.
- Rework the footer in the list.
- Optimize the file watcher.
### Fixed
- Fix bad contrast in the list on terminals with a light theme.
## 6.3.0 (2024-08-29)
### Added
- Add the following exercise lints:
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
- `forbid(todo)`: You forgot a `todo!()`.
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
- Show a link to every exercise file in the list.
- Add scroll padding in the list.
- Break the help footer of the list into two lines when the terminal width isn't big enough.
- Enable scrolling with the mouse in the list.
- `dev check`: Show the progress of checks.
- `dev check`: Check that the length of all exercise names is lower than 32.
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
### Changed
- The compilation time when installing Rustlings is reduced.
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
- Better highlighting of the solution file after an exercise is done.
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
- Be explicit about `q` only quitting the list and not the whole program in the list.
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
- Ignore the standard output of `git init`.
- `threads3`: Remove the queue length and improve tests.
- `errors4`: Use match instead of a comparison chain in the solution.
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
### Fixed
- Fix the error on some systems about too many open files during the final check of all exercises.
- Fix the list when the terminal height is too low.
- Restore the terminal after an error in the list.
## 6.2.0 (2024-08-09) ## 6.2.0 (2024-08-09)
@ -107,11 +16,13 @@
- Run the final check of all exercises in parallel. - Run the final check of all exercises in parallel.
- Small exercise improvements. - Small exercise improvements.
<a name="6.1.0"></a>
## 6.1.0 (2024-07-10) ## 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 (including third-party ones) include at least one `TODO` comment.
- `dev check`: Check that all exercises actually fail to run (not already solved). - `dev check`: Check that all exercises actually fail to run (not already solved).
#### Changed #### Changed
@ -124,11 +35,15 @@
- Exit with a helpful error message on missing/unsupported terminal/TTY. - Exit with a helpful error message on missing/unsupported terminal/TTY.
- Mark the last exercise as done. - Mark the last exercise as done.
<a name="6.0.1"></a>
## 6.0.1 (2024-07-04) ## 6.0.1 (2024-07-04)
Small exercise improvements and fixes. Small exercise improvements and fixes.
Most importantly, fixed that the exercise `clippy1` was already solved 😅 Most importantly, fixed that the exercise `clippy1` was already solved 😅
<a name="6.0.0"></a>
## 6.0.0 (2024-07-03) ## 6.0.0 (2024-07-03)
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨ This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
@ -157,7 +72,7 @@ You can read about the motivations of this change in [this issue](https://github
### List mode ### List mode
A new list mode was added! A list mode was added using [Ratatui](https://ratatui.rs).
You can enter it by entering `l` in the watch mode. You can enter it by entering `l` in the watch mode.
It offers the following features: It offers the following features:
@ -186,13 +101,15 @@ This should avoid issues related to the language server or to running exercises,
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎 Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
Make Clippy your friend from early on 🥰 Make Clippy your friend from early on 🥰
### Community Exercises ### Third-party exercises
Rustlings now supports community exercises! Rustlings now supports third-party exercises!
Do you want to create your own set of Rustlings exercises to focus on some specific topic? Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises? Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [community exercises](https://rustlings.rust-lang.org/community-exercises)! Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
<a name="5.6.1"></a>
## 5.6.1 (2023-09-18) ## 5.6.1 (2023-09-18)
@ -209,6 +126,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- `as_ref_mut`: Fixed a typo in a test function name. - `as_ref_mut`: Fixed a typo in a test function name.
- `enums3`: Fixed formatting with `rustfmt`. - `enums3`: Fixed formatting with `rustfmt`.
<a name="5.6.0"></a>
## 5.6.0 (2023-09-04) ## 5.6.0 (2023-09-04)
#### Added #### Added
@ -248,12 +167,16 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Lots of Nix housekeeping that I don't feel qualified to write about! - Lots of Nix housekeeping that I don't feel qualified to write about!
- Improved CI workflows, we're now testing on multiple platforms at once. - Improved CI workflows, we're now testing on multiple platforms at once.
<a name="5.5.1"></a>
## 5.5.1 (2023-05-17) ## 5.5.1 (2023-05-17)
#### Fixed #### Fixed
- Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix.
<a name="5.5.0"></a>
## 5.5.0 (2023-05-17) ## 5.5.0 (2023-05-17)
#### Added #### Added
@ -288,6 +211,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Added a markdown linter to run on GitHub actions - Added a markdown linter to run on GitHub actions
- Split quick installation section into two code blocks - Split quick installation section into two code blocks
<a name="5.4.1"></a>
## 5.4.1 (2023-03-10) ## 5.4.1 (2023-03-10)
#### Changed #### Changed
@ -303,6 +228,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]` - `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]`
- `cli`: Actually show correct progress percentages - `cli`: Actually show correct progress percentages
<a name="5.4.0"></a>
## 5.4.0 (2023-02-12) ## 5.4.0 (2023-02-12)
#### Changed #### Changed
@ -331,6 +258,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Bumped min Rust version to 1.58 in installation script - Bumped min Rust version to 1.58 in installation script
<a name="5.3.0"></a>
## 5.3.0 (2022-12-23) ## 5.3.0 (2022-12-23)
#### Added #### Added
@ -363,6 +292,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Applied some Clippy and rustfmt formatting - Applied some Clippy and rustfmt formatting
- Added a note on Windows PowerShell and other shell compatibility - Added a note on Windows PowerShell and other shell compatibility
<a name="5.2.1"></a>
## 5.2.1 (2022-09-06) ## 5.2.1 (2022-09-06)
#### Fixed #### Fixed
@ -376,6 +307,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Fixed a typo in README.md - Fixed a typo in README.md
<a name="5.2.0"></a>
## 5.2.0 (2022-08-27) ## 5.2.0 (2022-08-27)
#### Added #### Added
@ -392,12 +325,16 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **quiz1**: Adjusted the explanations to be consistent with - **quiz1**: Adjusted the explanations to be consistent with
the tests the tests
<a name="5.1.1"></a>
## 5.1.1 (2022-08-17) ## 5.1.1 (2022-08-17)
#### Bug Fixes #### Bug Fixes
- Fixed an incorrect assertion in options1 - Fixed an incorrect assertion in options1
<a name="5.1.0"></a>
## 5.1.0 (2022-08-16) ## 5.1.0 (2022-08-16)
#### Features #### Features
@ -432,6 +369,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Clarified manual installation instructions using `cargo install --path .` - Clarified manual installation instructions using `cargo install --path .`
- Added a link to our Zulip in the readme file - Added a link to our Zulip in the readme file
<a name="5.0.0"></a>
## 5.0.0 (2022-07-16) ## 5.0.0 (2022-07-16)
#### Features #### Features
@ -504,6 +443,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Updated spacing in Cargo.toml. - Updated spacing in Cargo.toml.
- Added a GitHub actions config so that tests run on every PR/commit. - Added a GitHub actions config so that tests run on every PR/commit.
<a name="4.8.0"></a>
## 4.8.0 (2022-07-01) ## 4.8.0 (2022-07-01)
#### Features #### Features
@ -524,6 +465,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting. - Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting.
- Removed the deprecated Rust GitPod extension. - Removed the deprecated Rust GitPod extension.
<a name="4.7.1"></a>
## 4.7.1 (2022-04-20) ## 4.7.1 (2022-04-20)
#### Features #### Features
@ -544,6 +487,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- The changelog will now be manually written instead of being automatically generated by the - The changelog will now be manually written instead of being automatically generated by the
Git log. Git log.
<a name="4.7.0"></a>
## 4.7.0 (2022-04-14) ## 4.7.0 (2022-04-14)
#### Features #### Features
@ -584,6 +529,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886)) - 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)) - Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929))
<a name="4.6.0"></a>
## 4.6.0 (2021-09-25) ## 4.6.0 (2021-09-25)
#### Features #### Features
@ -606,6 +553,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1)) - Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1))
- **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a)) - **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a))
<a name="4.5.0"></a>
## 4.5.0 (2021-07-07) ## 4.5.0 (2021-07-07)
#### Features #### Features
@ -626,6 +575,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e)) - **try_from_into, from_str:** hints for dyn Error ([11d2cf0d](https://github.com/rust-lang/rustlings/commit/11d2cf0d604dee3f5023c17802d69438e69fa50e))
- **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5)) - **variables5:** confine the answer further ([48ffcbd2](https://github.com/rust-lang/rustlings/commit/48ffcbd2c4cc4d936c2c7480019190f179813cc5))
<a name="4.4.0"></a>
## 4.4.0 (2021-04-24) ## 4.4.0 (2021-04-24)
#### Bug Fixes #### Bug Fixes
@ -667,6 +618,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913)) - updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913))
- added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4)) - added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4))
<a name="4.3.0"></a>
## 4.3.0 (2020-12-29) ## 4.3.0 (2020-12-29)
#### Features #### Features
@ -689,6 +642,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755)) - Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755))
- **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695)) - **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695))
<a name="4.2.0"></a>
## 4.2.0 (2020-11-07) ## 4.2.0 (2020-11-07)
#### Features #### Features
@ -709,6 +664,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e)) - missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e))
- **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6)) - **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6))
<a name="4.1.0"></a>
## 4.1.0 (2020-10-05) ## 4.1.0 (2020-10-05)
#### Bug Fixes #### Bug Fixes
@ -731,6 +688,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b)) - **cli:** Added 'cls' command to 'watch' mode (#474) ([4f2468e1](https://github.com/rust-lang/rustlings/commit/4f2468e14f574a93a2e9b688367b5752ed96ae7b))
- **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5)) - **try_from_into:** Add insufficient length test (#469) ([523d18b8](https://github.com/rust-lang/rustlings/commit/523d18b873a319f7c09262f44bd40e2fab1830e5))
<a name="4.0.0"></a>
## 4.0.0 (2020-07-08) ## 4.0.0 (2020-07-08)
#### Breaking Changes #### Breaking Changes
@ -772,6 +731,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45)) - **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45))
- **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094)) - **variables6:** minor typo (#419) ([524e17df](https://github.com/rust-lang/rustlings/commit/524e17df10db95f7b90a0f75cc8997182a8a4094))
<a name="3.0.0"></a>
## 3.0.0 (2020-04-11) ## 3.0.0 (2020-04-11)
#### Breaking Changes #### Breaking Changes
@ -794,6 +755,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229)) - 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)) - **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71))
<a name="2.2.1"></a>
### 2.2.1 (2020-02-27) ### 2.2.1 (2020-02-27)
#### Bug Fixes #### Bug Fixes
@ -804,11 +767,13 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921)) - Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921))
<a name="2.2.0"></a>
## 2.2.0 (2020-02-25) ## 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)) - Update deps to version compatable with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401))
- **docs:** - **docs:**
- Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f)) - Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f))
- Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9)) - Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9))
@ -831,6 +796,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Added traits exercises (#274 but specifically #216, which originally added - Added traits exercises (#274 but specifically #216, which originally added
this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17)) this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17))
<a name="2.1.0"></a>
## 2.1.0 (2019-11-27) ## 2.1.0 (2019-11-27)
#### Bug Fixes #### Bug Fixes
@ -848,6 +815,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c)) - **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c))
<a name="2.0.0"></a>
## 2.0.0 (2019-11-12) ## 2.0.0 (2019-11-12)
#### Bug Fixes #### Bug Fixes
@ -868,6 +837,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425)) - **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)) - **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f))
<a name="1.5.1"></a>
### 1.5.1 (2019-11-11) ### 1.5.1 (2019-11-11)
#### Bug Fixes #### Bug Fixes
@ -879,6 +850,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205)) - **threads:** Move Threads behind SLT ([fbe91a67](https://github.com/rust-lang/rustlings/commit/fbe91a67a482bfe64cbcdd58d06ba830a0f39da3), closes [#205](https://github.com/rust-lang/rustlings/issues/205))
- **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648)) - **watch:** clear screen before each `verify()` ([3aff590](https://github.com/rust-lang/rustlings/commit/3aff59085586c24196a547c2693adbdcf4432648))
<a name="1.5.0"></a>
## 1.5.0 (2019-11-09) ## 1.5.0 (2019-11-09)
#### Bug Fixes #### Bug Fixes
@ -903,6 +876,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959)) - 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)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
<a name="1.4.1"></a>
### 1.4.1 (2019-08-13) ### 1.4.1 (2019-08-13)
#### Bug Fixes #### Bug Fixes
@ -911,6 +886,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) - **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6))
- **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446))
<a name="1.4.0"></a>
## 1.4.0 (2019-07-13) ## 1.4.0 (2019-07-13)
#### Bug Fixes #### Bug Fixes
@ -927,6 +904,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724)) - **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)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031))
<a name="1.3.0"></a>
### 1.3.0 (2019-06-05) ### 1.3.0 (2019-06-05)
#### Features #### Features
@ -942,12 +921,16 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Fix broken link (#164, @HanKruiger) - Fix broken link (#164, @HanKruiger)
- Remove highlighting and syntect (#167, @komaeda) - Remove highlighting and syntect (#167, @komaeda)
<a name="1.2.2"></a>
### 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 - Reverted `--nocapture` flag since it was causing tests to pass unconditionally
<a name="1.2.1"></a>
### 1.2.1 (2019-04-22) ### 1.2.1 (2019-04-22)
#### Bug Fixes #### Bug Fixes
@ -955,6 +938,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Fix the `--nocapture` feature (@komaeda) - Fix the `--nocapture` feature (@komaeda)
- Provide a nicer error message for when you're in the wrong directory - Provide a nicer error message for when you're in the wrong directory
<a name="1.2.0"></a>
### 1.2.0 (2019-04-22) ### 1.2.0 (2019-04-22)
#### Features #### Features
@ -962,6 +947,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Add errors to exercises that compile without user changes (@yvan-sraka) - Add errors to exercises that compile without user changes (@yvan-sraka)
- Use --nocapture when testing, enabling `println!` when running (@komaeda) - Use --nocapture when testing, enabling `println!` when running (@komaeda)
<a name="1.1.1"></a>
### 1.1.1 (2019-04-14) ### 1.1.1 (2019-04-14)
#### Bug fixes #### Bug fixes
@ -974,6 +961,8 @@ Then follow the link to the guide about [community exercises](https://rustlings.
- Fix links by deleting book version (@diodfr, #142) - Fix links by deleting book version (@diodfr, #142)
- Canonicalize paths to fix path matching (@cjpearce, #143) - Canonicalize paths to fix path matching (@cjpearce, #143)
<a name="1.1.0"></a>
### 1.1.0 (2019-03-20) ### 1.1.0 (2019-03-20)
- errors2.rs: update link to Rust book (#124) - errors2.rs: update link to Rust book (#124)
@ -983,12 +972,16 @@ 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) - Give a warning when Rustlings isn't run from the right directory (#123)
- Verify that rust version is recent enough to install Rustlings (#131) - Verify that rust version is recent enough to install Rustlings (#131)
<a name="1.0.1"></a>
### 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`) - 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) - 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 - Reworks the exercise management to use an external TOML file instead of just listing them in the code
<a name="1.0.0"></a>
### 1.0.0 (2019-03-06) ### 1.0.0 (2019-03-06)
Initial release. Initial release.

716
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
[workspace] [workspace]
resolver = "2"
exclude = [ exclude = [
"tests/test_exercises", "tests/test_exercises",
"dev", "dev",
] ]
[workspace.package] [workspace.package]
version = "6.5.0" version = "6.2.0"
authors = [ authors = [
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it "Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal "Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
@ -14,12 +15,12 @@ authors = [
] ]
repository = "https://github.com/rust-lang/rustlings" repository = "https://github.com/rust-lang/rustlings"
license = "MIT" license = "MIT"
edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`. edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
rust-version = "1.88" rust-version = "1.80"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0.205", features = ["derive"] }
toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -45,20 +46,19 @@ include = [
] ]
[dependencies] [dependencies]
anyhow = "1.0" ahash = { version = "0.8.11", default-features = false }
clap = { version = "4.5", features = ["derive"] } anyhow = "1.0.86"
crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] } clap = { version = "4.5.14", features = ["derive"] }
notify = "8.0" notify-debouncer-mini = { version = "0.4.1", default-features = false }
rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } os_pipe = "1.2.1"
serde_json = "1.0" ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" }
serde_json = "1.0.122"
serde.workspace = true serde.workspace = true
toml.workspace = true toml_edit.workspace = true
[target.'cfg(not(windows))'.dependencies]
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.21" tempfile = "3.12.0"
[profile.release] [profile.release]
panic = "abort" panic = "abort"
@ -68,20 +68,7 @@ panic = "abort"
[package.metadata.release] [package.metadata.release]
pre-release-hook = ["./release-hook.sh"] pre-release-hook = ["./release-hook.sh"]
pre-release-commit-message = "Release 🎉"
[workspace.lints.rust] # TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
unsafe_code = "forbid" [lints.clippy]
unstable_features = "forbid" needless_option_as_deref = "allow"
[workspace.lints.clippy]
empty_loop = "forbid"
disallowed-types = "deny"
disallowed-methods = "deny"
infinite_loop = "deny"
mem_forget = "deny"
dbg_macro = "warn"
todo = "warn"
[lints]
workspace = true

157
README.md
View File

@ -1,7 +1,156 @@
# [Rustlings](https://rustlings.rust-lang.org) 🦀 <div class="oranda-hide">
Small exercises to get you used to reading and writing [Rust](https://www.rust-lang.org) code - _Recommended in parallel to reading [the official Rust book](https://doc.rust-lang.org/book) 📚_ # Rustlings 🦀❤️
Visit the **website** for a demo, info about setup and more: </div>
## ➡️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️ Greetings and welcome to Rustlings.
This project contains small exercises to get you used to reading and writing Rust code.
This includes reading and responding to compiler messages!
It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️
[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful.
It contains code examples and exercises similar to Rustlings, but online.
## Getting Started
### Installing Rust
Before installing Rustlings, you need to have the **latest version of Rust** installed.
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
This will also install _Cargo_, Rust's package/project manager.
> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker).
>
> Deb: `sudo apt install gcc`.
> Dnf: `sudo dnf install gcc`.
> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`.
### Installing Rustlings
The following command will download and compile Rustlings:
```bash
cargo install rustlings
```
<details>
<summary><strong>If the installation fails…</strong> (<em>click to expand</em>)</summary>
- Make sure you have the latest Rust version by running `rustup update`
- Try adding the `--locked` flag: `cargo install rustlings --locked`
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
</details>
### Initialization
After installing Rustlings, run the following command to initialize the `rustlings/` directory:
```bash
rustlings init
```
<details>
<summary><strong>If the command <code>rustlings</code> can't be found…</strong> (<em>click to expand</em>)</summary>
You are probably using Linux and installed Rust using your package manager.
Cargo installs binaries to the directory `~/.cargo/bin`.
Sadly, package managers often don't add `~/.cargo/bin` to your `PATH` environment variable.
The solution is to …
- either add `~/.cargo/bin` manually to `PATH`
- or to uninstall Rust from the package manager and install it using the official way with `rustup`: https://www.rust-lang.org/tools/install
</details>
Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises:
```bash
cd rustlings/
rustlings
```
## Working environment
### Editor
Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises.
### Terminal
While working with Rustlings, please use a modern terminal for the best user experience.
The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.
For every topic, there is an additional `README.md` file with some resources to get you started on the topic.
We highly recommend that you have a look at them before you start 📚️
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
Some exercises contain tests that need to pass for the exercise to be done ✅
Search for `TODO` and `todo!()` to find out what you need to change.
Ask for hints by entering `h` in the _watch mode_ 💡
### Watch Mode
After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`.
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
<details>
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
>
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
</details>
### Exercise List
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
The list allows you to…
- See the status of all exercises (done or pending)
- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one)
- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards)
See the footer of the list for all possible keys.
## Continuing On
Once you've completed Rustlings, put your new knowledge to good use!
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
## Third-Party Exercises
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)!
## Uninstalling Rustlings
If you want to remove Rustlings from your system, run the following command:
```bash
cargo uninstall rustlings
```
## Contributing
See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗
## Contributors ✨
Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉

53
THIRD_PARTY_EXERCISES.md Normal file
View File

@ -0,0 +1,53 @@
# Third-Party Exercises
The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic.
You could also offer a translation of the original Rustlings exercises as third-party exercises.
## Getting started
To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started.
Read the comments in the generated `info.toml` file to understand its format.
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
## Create an exercise
Here is an example of the metadata of one file:
```toml
[[exercises]]
name = "intro1"
hint = """
To finish this exercise, you need to …
This link might help you …"""
```
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
The exercise needs to contain a `main` function, but it can be empty.
Adding tests is recommended.
Look at the official Rustlings exercises for inspiration.
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
Now, run `rustlings dev check`.
It will tell you about any issues with your exercises.
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
That's it!
You finished your first exercise 🎉
## Publish
Now, add more exercises and publish them as a Git repository.
Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones.
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
But you can trust the users to not look at the solution too early 😉
## Share
After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃

View File

@ -1,5 +0,0 @@
fn main() {
// Fix building from source on Windows because it can't handle file links.
#[cfg(windows)]
let _ = std::fs::copy("dev/Cargo.toml", "dev-Cargo.toml");
}

View File

@ -1,11 +0,0 @@
disallowed-types = [
{ path = "crossterm::style::Stylize", reason = "inefficient, use `.queue(…)` instead" },
{ path = "crossterm::style::styled_content::StyledContent", reason = "inefficient, use `.queue(…)` instead" },
]
disallowed-methods = [
{ path = "crossterm::style::style", reason = "inefficient, use `.queue(…)` instead" },
{ path = "std::thread::spawn", replacement = "std::thread::Builder::spawn", reason = "handle the error" },
{ path = "std::thread::Scope::spawn", replacement = "std::thread::Builder::spawn", reason = "handle the error" },
{ path = "std::process::exit", replacement = "std::process::ExitCode" },
]

View File

@ -1,4 +1,4 @@
# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`. # Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`.
bin = [ bin = [
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro1", path = "../exercises/00_intro/intro1.rs" },
{ name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" }, { name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" },
@ -192,7 +192,7 @@ bin = [
[package] [package]
name = "exercises" name = "exercises"
edition = "2024" edition = "2021"
# Don't publish the exercises on crates.io! # Don't publish the exercises on crates.io!
publish = false publish = false
@ -201,23 +201,3 @@ panic = "abort"
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
[lints.rust]
# You shouldn't write unsafe code in Rustlings!
unsafe_code = "forbid"
# You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
unstable_features = "forbid"
# Dead code warnings can't be avoided in some exercises and might distract while learning.
dead_code = "allow"
[lints.clippy]
# You forgot a `todo!()`!
todo = "forbid"
# This can only happen by mistake in Rustlings.
empty_loop = "forbid"
# No infinite loops are needed in Rustlings.
infinite_loop = "deny"
# You shouldn't leak memory while still learning Rust!
mem_forget = "deny"
# Currently, there are no disallowed methods. This line avoids problems when developing Rustlings.
disallowed_methods = "allow"

View File

@ -1,4 +1,4 @@
// TODO: We sometimes encourage you to keep trying things on a given exercise // TODO: We sometimes encourage you to keep trying things on a given exercise,
// even after you already figured it out. If you got everything working and feel // even after you already figured it out. If you got everything working and feel
// ready for the next exercise, enter `n` in the terminal. // ready for the next exercise, enter `n` in the terminal.
// //
@ -6,7 +6,8 @@
// Try adding a new `println!` and check the updated output in the terminal. // Try adding a new `println!` and check the updated output in the terminal.
fn main() { fn main() {
println!(r#" Welcome to... "#); println!("Hello and");
println!(r#" welcome to... "#);
println!(r#" _ _ _ "#); println!(r#" _ _ _ "#);
println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#); println!(r#" _ __ _ _ ___| |_| (_)_ __ __ _ ___ "#);
println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#); println!(r#" | '__| | | / __| __| | | '_ \ / _` / __| "#);

View File

@ -1,7 +1,7 @@
# Variables # Variables
In Rust, variables are immutable by default. In Rust, variables are immutable by default.
When a variable is immutable, once a value is bound to a name, you can't change that value. When a variable is immutable, once a value is bound to a name, you cant change that value.
You can make them mutable by adding `mut` in front of the variable name. You can make them mutable by adding `mut` in front of the variable name.
## Further information ## Further information

View File

@ -1,5 +1,5 @@
fn main() { fn main() {
// TODO: Add the missing keyword. // TODO: Add missing keyword.
x = 5; x = 5;
println!("x has the value {x}"); println!("x has the value {x}");

View File

@ -1,6 +1,6 @@
fn main() { fn main() {
let number = "T-H-R-E-E"; // Don't change this line let number = "T-H-R-E-E"; // Don't change this line
println!("Spell a number: {number}"); println!("Spell a number: {}", number);
// TODO: Fix the compiler error by changing the line below without renaming the variable. // TODO: Fix the compiler error by changing the line below without renaming the variable.
number = 3; number = 3;

View File

@ -1,4 +1,4 @@
fn call_me(num: u8) { fn call_me(num: u32) {
for i in 0..num { for i in 0..num {
println!("Ring! Call number {}", i + 1); println!("Ring! Call number {}", i + 1);
} }

View File

@ -1,7 +1,7 @@
// TODO: Fix the compiler error on this function. // TODO: Fix the compiler error on this function.
fn picky_eater(food: &str) -> &str { fn foo_if_fizz(fizzish: &str) -> &str {
if food == "strawberry" { if fizzish == "fizz" {
"Yummy!" "foo"
} else { } else {
1 1
} }
@ -18,20 +18,18 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn yummy_food() { fn foo_for_fizz() {
// This means that calling `picky_eater` with the argument "strawberry" should return "Yummy!". // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
assert_eq!(picky_eater("strawberry"), "Yummy!"); assert_eq!(foo_if_fizz("fizz"), "foo");
} }
#[test] #[test]
fn neutral_food() { fn bar_for_fuzz() {
assert_eq!(picky_eater("potato"), "I guess I can eat that."); assert_eq!(foo_if_fizz("fuzz"), "bar");
} }
#[test] #[test]
fn default_disliked_food() { fn default_to_baz() {
assert_eq!(picky_eater("broccoli"), "No thanks!"); assert_eq!(foo_if_fizz("literally anything"), "baz");
assert_eq!(picky_eater("gummy bears"), "No thanks!");
assert_eq!(picky_eater("literally anything"), "No thanks!");
} }
} }

View File

@ -9,6 +9,26 @@ fn vec_loop(input: &[i32]) -> Vec<i32> {
output output
} }
fn vec_map_example(input: &[i32]) -> Vec<i32> {
// An example of collecting a vector after mapping.
// We map each element of the `input` slice to its value plus 1.
// If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`.
input.iter().map(|element| element + 1).collect()
}
fn vec_map(input: &[i32]) -> Vec<i32> {
// TODO: Here, we also want to multiply each element in the `input` slice
// by 2, but with iterator mapping instead of manually pushing into an empty
// vector.
// See the example in the function `vec_map_example` above.
input
.iter()
.map(|element| {
// ???
})
.collect()
}
fn main() { fn main() {
// You can optionally experiment here. // You can optionally experiment here.
} }
@ -23,4 +43,18 @@ mod tests {
let ans = vec_loop(&input); let ans = vec_loop(&input);
assert_eq!(ans, [4, 8, 12, 16, 20]); assert_eq!(ans, [4, 8, 12, 16, 20]);
} }
#[test]
fn test_vec_map_example() {
let input = [1, 2, 3];
let ans = vec_map_example(&input);
assert_eq!(ans, [2, 3, 4]);
}
#[test]
fn test_vec_map() {
let input = [2, 4, 6, 8, 10];
let ans = vec_map(&input);
assert_eq!(ans, [4, 8, 12, 16, 20]);
}
} }

View File

@ -1,10 +1,10 @@
# Enums # Enums
Rust allows you to define types called "enums" which enumerate possible values. Rust allows you to define types called "enums" which enumerate possible values.
Enums are a feature in many languages, but their capabilities differ in each language. Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell. Enums are a feature in many languages, but their capabilities differ in each language. Rusts enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration. Useful in combination with enums is Rust's "pattern matching" facility, which makes it easy to run different code for different values of an enumeration.
## Further information ## Further information
- [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html) - [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html)
- [Pattern syntax](https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html) - [Pattern syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html)

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
struct Point { struct Point {
x: u64, x: u64,

View File

@ -4,11 +4,7 @@ struct Point {
} }
enum Message { enum Message {
Resize { width: u64, height: u64 }, // TODO: Implement the message variant types based on their usage below.
Move(Point),
Echo(String),
ChangeColor(u8, u8, u8),
Quit,
} }
struct State { struct State {

View File

@ -23,7 +23,6 @@ mod tests {
assert_eq!(trim_me("Hello! "), "Hello!"); assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!"); assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!"); assert_eq!(trim_me(" Hola! "), "Hola!");
assert_eq!(trim_me("Hi!"), "Hi!");
} }
#[test] #[test]

View File

@ -1,6 +1,7 @@
// You can bring module paths into scopes and provide new names for them with // You can bring module paths into scopes and provide new names for them with
// the `use` and `as` keywords. // the `use` and `as` keywords.
#[allow(dead_code)]
mod delicious_snacks { mod delicious_snacks {
// TODO: Add the following two `use` statements after fixing them. // TODO: Add the following two `use` statements after fixing them.
// use self::fruits::PEAR as ???; // use self::fruits::PEAR as ???;

View File

@ -17,7 +17,7 @@ struct TeamScores {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::<&str, TeamScores>::new(); let mut scores = HashMap::new();
for line in results.lines() { for line in results.lines() {
let mut split_iterator = line.split(','); let mut split_iterator = line.split(',');

View File

@ -1,8 +1,8 @@
// This function returns how much ice cream there is left in the fridge. // This function returns how much icecream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no ice cream is left (value 0). Return `None` if // someone eats it all, so no icecream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23. // `hour_of_day` is higher than 23.
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> { fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
// TODO: Complete the function body. // TODO: Complete the function body.
} }
@ -18,19 +18,19 @@ mod tests {
fn raw_value() { fn raw_value() {
// TODO: Fix this test. How do you get the value contained in the // TODO: Fix this test. How do you get the value contained in the
// Option? // Option?
let ice_creams = maybe_ice_cream(12); let icecreams = maybe_icecream(12);
assert_eq!(ice_creams, 5); // Don't change this line. assert_eq!(icecreams, 5); // Don't change this line.
} }
#[test] #[test]
fn check_ice_cream() { fn check_icecream() {
assert_eq!(maybe_ice_cream(0), Some(5)); assert_eq!(maybe_icecream(0), Some(5));
assert_eq!(maybe_ice_cream(9), Some(5)); assert_eq!(maybe_icecream(9), Some(5));
assert_eq!(maybe_ice_cream(18), Some(5)); assert_eq!(maybe_icecream(18), Some(5));
assert_eq!(maybe_ice_cream(22), Some(0)); assert_eq!(maybe_icecream(22), Some(0));
assert_eq!(maybe_ice_cream(23), Some(0)); assert_eq!(maybe_icecream(23), Some(0));
assert_eq!(maybe_ice_cream(24), None); assert_eq!(maybe_icecream(24), None);
assert_eq!(maybe_ice_cream(25), None); assert_eq!(maybe_icecream(25), None);
} }
} }

View File

@ -9,7 +9,7 @@ fn main() {
// TODO: Fix the compiler error by adding something to this match statement. // TODO: Fix the compiler error by adding something to this match statement.
match optional_point { match optional_point {
Some(p) => println!("Coordinates are {},{}", p.x, p.y), Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
_ => panic!("No match!"), _ => panic!("No match!"),
} }

View File

@ -1,8 +1,8 @@
# Error handling # Error handling
Most errors aren't serious enough to require the program to stop entirely. Most errors arent serious enough to require the program to stop entirely.
Sometimes, when a function fails, it's for a reason that you can easily interpret and respond to. Sometimes, when a function fails, its for a reason that you can easily interpret and respond to.
For example, if you try to open a file and that operation fails because the file doesn't exist, you might want to create the file instead of terminating the process. For example, if you try to open a file and that operation fails because the file doesnt exist, you might want to create the file instead of terminating the process.
## Further information ## Further information

View File

@ -6,7 +6,7 @@
// of `Option<String>`. // of `Option<String>`.
fn generate_nametag_text(name: String) -> Option<String> { fn generate_nametag_text(name: String) -> Option<String> {
if name.is_empty() { if name.is_empty() {
// Empty names aren't allowed // Empty names aren't allowed.
None None
} else { } else {
Some(format!("Hi! My name is {name}")) Some(format!("Hi! My name is {name}"))

View File

@ -1,3 +1,5 @@
#![allow(clippy::comparison_chain)]
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
enum CreationError { enum CreationError {
Negative, Negative,
@ -10,7 +12,6 @@ struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger { impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> { fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`. // TODO: This function shouldn't always return an `Ok`.
// Read the tests below to clarify what should be returned.
Ok(Self(value as u64)) Ok(Self(value as u64))
} }
} }

View File

@ -6,7 +6,7 @@
// //
// In short, this particular use case for boxes is for when you want to own a // 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 // 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<dyn Trait>` where // trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
// `Trait` is the trait the compiler looks for on any value used in that // `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 // context. For this exercise, that context is the potential errors which
// can be returned in a `Result`. // can be returned in a `Result`.

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
trait Licensed { trait Licensed {
// TODO: Add a default implementation for `licensing_info` so that // TODO: Add a default implementation for `licensing_info` so that
// implementors like the two structs below can share that default behavior // implementors like the two structs below can share that default behavior

View File

@ -39,8 +39,6 @@ mod tests {
#[test] #[test]
fn test_success() { fn test_success() {
assert_eq!(divide(81, 9), Ok(9)); assert_eq!(divide(81, 9), Ok(9));
assert_eq!(divide(81, -1), Ok(-81));
assert_eq!(divide(i64::MIN, i64::MIN), Ok(1));
} }
#[test] #[test]

View File

@ -8,6 +8,7 @@ use std::rc::Rc;
#[derive(Debug)] #[derive(Debug)]
struct Sun; struct Sun;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum Planet { enum Planet {
Mercury(Rc<Sun>), Mercury(Rc<Sun>),

View File

@ -1,5 +1,5 @@
// This program spawns multiple threads that each runs for at least 250ms, and // This program spawns multiple threads that each run for at least 250ms, and
// each thread returns how much time it took to complete. The program should // each thread returns how much time they took to complete. The program should
// wait until all the spawned threads have finished and should collect their // wait until all the spawned threads have finished and should collect their
// return values into a vector. // return values into a vector.

View File

@ -1,6 +1,7 @@
use std::{sync::mpsc, thread, time::Duration}; use std::{sync::mpsc, thread, time::Duration};
struct Queue { struct Queue {
length: u32,
first_half: Vec<u32>, first_half: Vec<u32>,
second_half: Vec<u32>, second_half: Vec<u32>,
} }
@ -8,6 +9,7 @@ struct Queue {
impl Queue { impl Queue {
fn new() -> Self { fn new() -> Self {
Self { Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5], first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10], second_half: vec![6, 7, 8, 9, 10],
} }
@ -46,15 +48,17 @@ mod tests {
fn threads3() { fn threads3() {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let queue = Queue::new(); let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx); send_tx(queue, tx);
let mut received = Vec::with_capacity(10); let mut total_received: u32 = 0;
for value in rx { for received in rx {
received.push(value); println!("Got: {received}");
total_received += 1;
} }
received.sort(); println!("Number of received values: {total_received}");
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); assert_eq!(total_received, queue_length);
} }
} }

View File

@ -10,6 +10,5 @@ of exercises to Rustlings, but is all about learning to write Macros.
## Further information ## Further information
- [The Rust Book - Macros](https://doc.rust-lang.org/book/ch20-05-macros.html) - [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html)
- [The Little Book of Rust Macros](https://veykril.github.io/tlborm/) - [The Little Book of Rust Macros](https://veykril.github.io/tlborm/)
- [Rust by Example - macro_rules!](https://doc.rust-lang.org/rust-by-example/macros.html)

View File

@ -1,25 +1,22 @@
// Here are some more easy Clippy fixes so you can see its utility. // Here are some more easy Clippy fixes so you can see its utility 📎
// TODO: Fix all the Clippy lints. // TODO: Fix all the Clippy lints.
#[rustfmt::skip]
#[allow(unused_variables, unused_assignments)] #[allow(unused_variables, unused_assignments)]
fn main() { fn main() {
let my_option: Option<&str> = None; let my_option: Option<()> = None;
// Assume that you don't know the value of `my_option`.
// In the case of `Some`, we want to print its value.
if my_option.is_none() { if my_option.is_none() {
println!("{}", my_option.unwrap()); println!("{:?}", my_option.unwrap());
} }
#[rustfmt::skip]
let my_arr = &[ let my_arr = &[
-1, -2, -3 -1, -2, -3
-4, -5, -6 -4, -5, -6
]; ];
println!("My array! Here it is: {my_arr:?}"); println!("My array! Here it is: {my_arr:?}");
let mut my_vec = vec![1, 2, 3, 4, 5]; let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5);
my_vec.resize(0, 5); println!("This Vec is empty, see? {my_empty_vec:?}");
println!("This Vec is empty, see? {my_vec:?}");
let mut value_a = 45; let mut value_a = 45;
let mut value_b = 66; let mut value_b = 66;

View File

@ -2,11 +2,10 @@
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Obtain the number of bytes (not characters) in the given argument // Obtain the number of bytes (not characters) in the given argument.
// (`.len()` returns the number of bytes in a string).
// TODO: Add the `AsRef` trait appropriately as a trait bound. // TODO: Add the `AsRef` trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize { fn byte_counter<T>(arg: T) -> usize {
arg.as_ref().len() arg.as_ref().as_bytes().len()
} }
// Obtain the number of characters (not bytes) in the given argument. // Obtain the number of characters (not bytes) in the given argument.

View File

@ -25,7 +25,7 @@ enum ParsePersonError {
ParseInt(ParseIntError), ParseInt(ParseIntError),
} }
// TODO: Complete this `FromStr` implementation to be able to parse a `Person` // TODO: Complete this `From` implementation to be able to parse a `Person`
// out of a string in the form of "Mark,20". // 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 // Note that you'll need to parse the age component into a `u8` with something
// like `"4".parse::<u8>()`. // like `"4".parse::<u8>()`.

View File

@ -9,7 +9,7 @@
| vecs | §8.1 | | vecs | §8.1 |
| move_semantics | §4.1-2 | | move_semantics | §4.1-2 |
| structs | §5.1, §5.3 | | structs | §5.1, §5.3 |
| enums | §6, §19.3 | | enums | §6, §18.3 |
| strings | §8.2 | | strings | §8.2 |
| modules | §7 | | modules | §7 |
| hashmaps | §8.3 | | hashmaps | §8.3 |
@ -22,6 +22,6 @@
| iterators | §13.2-4 | | iterators | §13.2-4 |
| smart_pointers | §15, §16.3 | | smart_pointers | §15, §16.3 |
| threads | §16.1-3 | | threads | §16.1-3 |
| macros | §20.5 | | macros | §19.5 |
| clippy | Appendix D | | clippy | §21.4 |
| conversions | n/a | | conversions | n/a |

13
oranda.json Normal file
View File

@ -0,0 +1,13 @@
{
"project": {
"homepage": "https://rustlings.cool",
"repository": "https://github.com/rust-lang/rustlings"
},
"marketing": {
"analytics": {
"plausible": {
"domain": "rustlings.cool"
}
}
}
}

View File

@ -9,8 +9,5 @@ cargo upgrades
# Similar to CI # Similar to CI
cargo clippy -- --deny warnings cargo clippy -- --deny warnings
cargo fmt --all --check cargo fmt --all --check
cargo test --workspace cargo test --workspace --all-targets
cargo dev check --require-solutions cargo run -- dev check --require-solutions
# MSRV
cargo +1.88 dev check --require-solutions

View File

@ -16,9 +16,6 @@ include = [
proc-macro = true proc-macro = true
[dependencies] [dependencies]
quote = "1.0" quote = "1.0.36"
serde.workspace = true serde.workspace = true
toml.workspace = true toml_edit.workspace = true
[lints]
workspace = true

View File

@ -1,7 +1,6 @@
format_version = 1 format_version = 1
welcome_message = """ welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners!
Is this your first time? Don't worry, Rustlings is made for beginners!
We are going to teach you a lot of things about Rust, but before we can We are going to teach you a lot of things about Rust, but before we can
get started, here are some notes about how Rustlings operates: get started, here are some notes about how Rustlings operates:
@ -11,16 +10,15 @@ get started, here are some notes about how Rustlings operates:
and fix them! and fix them!
2. Make sure to have your editor open in the `rustlings/` directory. Rustlings 2. Make sure to have your editor open in the `rustlings/` directory. Rustlings
will show you the path of the current exercise under the progress bar. Open will show you the path of the current exercise under the progress bar. Open
the exercise file in your editor, fix errors and save the file. Rustlings the exercise file in your editor, fix errors and save the file. Rustlings will
will automatically detect the file change and rerun the exercise. If all automatically detect the file change and rerun the exercise. If all errors are
errors are fixed, Rustlings will ask you to move on to the next exercise. fixed, Rustlings will ask you to move on to the next exercise.
3. If you're stuck on an exercise, enter `h` to show a hint. 3. If you're stuck on an exercise, enter `h` to show a hint.
4. If an exercise doesn't make sense to you, feel free to open an issue on 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes,
sometimes, other learners do too so you can help each other out!""" other learners do too so you can help each other out!"""
final_message = """ final_message = """We hope you enjoyed learning about the various aspects of Rust!
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! You can also contribute your own exercises to help the greater community!
@ -122,10 +120,10 @@ dir = "01_variables"
test = false test = false
hint = """ hint = """
We know about variables and mutability, but there is another important type of We know about variables and mutability, but there is another important type of
variable available: constants. variables available: constants.
Constants are always immutable. They are declared with the keyword `const` Constants are always immutable. They are declared with the keyword `const` instead
instead of `let`. of `let`.
The type of Constants must always be annotated. The type of Constants must always be annotated.
@ -255,7 +253,7 @@ require you to type in 100 items (but you certainly can if you want!).
For example, you can do: For example, you can do:
``` ```
let array = ["Are we there yet?"; 100]; let array = ["Are we there yet?"; 10];
``` ```
Bonus: what are some other things you could have that would return `true` Bonus: what are some other things you could have that would return `true`
@ -318,7 +316,15 @@ of the Rust book to learn more."""
name = "vecs2" name = "vecs2"
dir = "05_vecs" dir = "05_vecs"
hint = """ hint = """
Use the `.push()` method on the vector to push new elements to it.""" In the first function, we create an empty vector and want to push new elements
to it.
In the second function, we map the values of the input and collect them into a vector.
After you've completed both functions, decide for yourself which approach you
like better.
What do you think is the more commonly used pattern under Rust developers?"""
# MOVE SEMANTICS # MOVE SEMANTICS
@ -326,8 +332,8 @@ Use the `.push()` method on the vector to push new elements to it."""
name = "move_semantics1" name = "move_semantics1"
dir = "06_move_semantics" dir = "06_move_semantics"
hint = """ hint = """
So you've got the "cannot borrow `vec` as mutable, as it is not declared as So you've got the "cannot borrow `vec` as mutable, as it is not declared as mutable"
mutable" error on the line where we push an element to the vector, right? error on the line where we push an element to the vector, right?
The fix for this is going to be adding one keyword, and the addition is NOT on The fix for this is going to be adding one keyword, and the addition is NOT on
the line where we push to the vector (where the error is). the line where we push to the vector (where the error is).
@ -363,8 +369,7 @@ hint = """
Carefully reason about the range in which each mutable reference is in Carefully reason about the range in which each mutable reference is in
scope. Does it help to update the value of `x` immediately after scope. Does it help to update the value of `x` immediately after
the mutable reference is taken? the mutable reference is taken?
Read more about 'Mutable References' in the book's section 'References and Read more about 'Mutable References' in the book's section 'References and Borrowing':
Borrowing':
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references.""" https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references."""
[[exercises]] [[exercises]]
@ -503,8 +508,7 @@ name = "strings4"
dir = "09_strings" dir = "09_strings"
test = false test = false
hint = """ hint = """
Replace `placeholder` with either `string` or `string_slice` in the `main` Replace `placeholder` with either `string` or `string_slice` in the `main` function.
function.
Example: Example:
`placeholder("blue");` `placeholder("blue");`
@ -566,8 +570,12 @@ https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-
name = "hashmaps3" name = "hashmaps3"
dir = "11_hashmaps" dir = "11_hashmaps"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_default()` methods of `HashMap` to insert the Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
default value of `TeamScores` if a team doesn't exist in the table yet. `HashMap` to insert the default value of `TeamScores` if a team doesn't
exist in the table yet.
Learn more in The Book:
https://doc.rust-lang.org/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value
Hint 2: If there is already an entry for a given key, the value returned by Hint 2: If there is already an entry for a given key, the value returned by
`entry()` can be updated based on the existing value. `entry()` can be updated based on the existing value.
@ -755,7 +763,7 @@ Notice how the trait takes ownership of `self` and returns `Self`.
Although the signature of `append_bar` in the trait takes `self` as argument, Although the signature of `append_bar` in the trait takes `self` as argument,
the implementation can take `mut self` instead. This is possible because the the implementation can take `mut self` instead. This is possible because the
value is owned anyway.""" the value is owned anyway."""
[[exercises]] [[exercises]]
name = "traits3" name = "traits3"
@ -1131,7 +1139,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
source of potential error. source of potential error.
See the suggestions of the Clippy warning in the compile output and use the See the suggestions of the Clippy warning in the compile output and use the
appropriate replacement constant from `std::f32::consts`.""" appropriate replacement constant from `std::f32::consts`..."""
[[exercises]] [[exercises]]
name = "clippy2" name = "clippy2"
@ -1192,8 +1200,7 @@ hint = """
Is there an implementation of `TryFrom` in the standard library that can both do Is there an implementation of `TryFrom` in the standard library that can both do
the required integer conversion and check the range of the input? the required integer conversion and check the range of the input?
Challenge: Can you make the `TryFrom` implementations generic over many integer Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
types?"""
[[exercises]] [[exercises]]
name = "as_ref_mut" name = "as_ref_mut"

View File

@ -16,7 +16,7 @@ struct InfoFile {
#[proc_macro] #[proc_macro]
pub fn include_files(_: TokenStream) -> TokenStream { pub fn include_files(_: TokenStream) -> TokenStream {
let info_file = include_str!("../info.toml"); let info_file = include_str!("../info.toml");
let exercises = toml::de::from_str::<InfoFile>(info_file) let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
.expect("Failed to parse `info.toml`") .expect("Failed to parse `info.toml`")
.exercises; .exercises;

View File

@ -1,6 +1,6 @@
fn main() { fn main() {
let number = "T-H-R-E-E"; let number = "T-H-R-E-E";
println!("Spell a number: {number}"); println!("Spell a number: {}", number);
// Using variable shadowing // Using variable shadowing
// https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing // https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing

View File

@ -1,4 +1,4 @@
fn call_me(num: u8) { fn call_me(num: u32) {
for i in 0..num { for i in 0..num {
println!("Ring! Call number {}", i + 1); println!("Ring! Call number {}", i + 1);
} }

View File

@ -1,5 +1,9 @@
fn bigger(a: i32, b: i32) -> i32 { fn bigger(a: i32, b: i32) -> i32 {
if a > b { a } else { b } if a > b {
a
} else {
b
}
} }
fn main() { fn main() {

View File

@ -1,10 +1,10 @@
fn picky_eater(food: &str) -> &str { fn foo_if_fizz(fizzish: &str) -> &str {
if food == "strawberry" { if fizzish == "fizz" {
"Yummy!" "foo"
} else if food == "potato" { } else if fizzish == "fuzz" {
"I guess I can eat that." "bar"
} else { } else {
"No thanks!" "baz"
} }
} }
@ -17,19 +17,17 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn yummy_food() { fn foo_for_fizz() {
assert_eq!(picky_eater("strawberry"), "Yummy!"); assert_eq!(foo_if_fizz("fizz"), "foo");
} }
#[test] #[test]
fn neutral_food() { fn bar_for_fuzz() {
assert_eq!(picky_eater("potato"), "I guess I can eat that."); assert_eq!(foo_if_fizz("fuzz"), "bar");
} }
#[test] #[test]
fn default_disliked_food() { fn default_to_baz() {
assert_eq!(picky_eater("broccoli"), "No thanks!"); assert_eq!(foo_if_fizz("literally anything"), "baz");
assert_eq!(picky_eater("gummy bears"), "No thanks!");
assert_eq!(picky_eater("literally anything"), "No thanks!");
} }
} }

View File

@ -8,6 +8,22 @@ fn vec_loop(input: &[i32]) -> Vec<i32> {
output output
} }
fn vec_map_example(input: &[i32]) -> Vec<i32> {
// An example of collecting a vector after mapping.
// We map each element of the `input` slice to its value plus 1.
// If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`.
input.iter().map(|element| element + 1).collect()
}
fn vec_map(input: &[i32]) -> Vec<i32> {
// We will dive deeper into iterators, but for now, this is all what you
// had to do!
// Advanced note: This method is more efficient because it automatically
// preallocates enough capacity. This can be done manually in `vec_loop`
// using `Vec::with_capacity(input.len())` instead of `Vec::new()`.
input.iter().map(|element| 2 * element).collect()
}
fn main() { fn main() {
// You can optionally experiment here. // You can optionally experiment here.
} }
@ -22,4 +38,18 @@ mod tests {
let ans = vec_loop(&input); let ans = vec_loop(&input);
assert_eq!(ans, [4, 8, 12, 16, 20]); assert_eq!(ans, [4, 8, 12, 16, 20]);
} }
#[test]
fn test_vec_map_example() {
let input = [1, 2, 3];
let ans = vec_map_example(&input);
assert_eq!(ans, [2, 3, 4]);
}
#[test]
fn test_vec_map() {
let input = [2, 4, 6, 8, 10];
let ans = vec_map(&input);
assert_eq!(ans, [4, 8, 12, 16, 20]);
}
} }

View File

@ -4,6 +4,8 @@ fn main() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// TODO: Fix the compiler errors only by reordering the lines in the test.
// Don't add, change or remove any line.
#[test] #[test]
fn move_semantics4() { fn move_semantics4() {
let mut x = Vec::new(); let mut x = Vec::new();

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
struct Point { struct Point {
x: u64, x: u64,

View File

@ -46,8 +46,8 @@ impl State {
match message { match message {
Message::Resize { width, height } => self.resize(width, height), Message::Resize { width, height } => self.resize(width, height),
Message::Move(point) => self.move_position(point), Message::Move(point) => self.move_position(point),
Message::Echo(string) => self.echo(string), Message::Echo(s) => self.echo(s),
Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue), Message::ChangeColor(r, g, b) => self.change_color(r, g, b),
Message::Quit => self.quit(), Message::Quit => self.quit(),
} }
} }

View File

@ -26,7 +26,6 @@ mod tests {
assert_eq!(trim_me("Hello! "), "Hello!"); assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!"); assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!"); assert_eq!(trim_me(" Hola! "), "Hola!");
assert_eq!(trim_me("Hi!"), "Hi!");
} }
#[test] #[test]

View File

@ -18,11 +18,12 @@ fn main() {
// Here, both answers work. // Here, both answers work.
// `.into()` converts a type into an expected type. // `.into()` converts a type into an expected type.
// If it is called where `String` is expected, it will convert `&str` to `String`. // If it is called where `String` is expected, it will convert `&str` to `String`.
// But if is called where `&str` is expected, then `&str` is kept `&str` since no
// conversion is needed.
string("nice weather".into()); 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_slice("nice weather".into());
// ^^^^^^^ the compiler recommends removing the `.into()`
// call because it is a useless conversion.
string(format!("Interpolation {}", "Station")); string(format!("Interpolation {}", "Station"));

View File

@ -1,3 +1,4 @@
#[allow(dead_code)]
mod delicious_snacks { mod delicious_snacks {
// Added `pub` and used the expected alias after `as`. // Added `pub` and used the expected alias after `as`.
pub use self::fruits::PEAR as fruit; pub use self::fruits::PEAR as fruit;

View File

@ -1,7 +1,7 @@
// A basket of fruits in the form of a hash map needs to be defined. The key // 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 // 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 // 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. // of all the fruits should be at least 5.
use std::collections::HashMap; use std::collections::HashMap;

View File

@ -17,7 +17,7 @@ struct TeamScores {
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::<&str, TeamScores>::new(); let mut scores = HashMap::new();
for line in results.lines() { for line in results.lines() {
let mut split_iterator = line.split(','); let mut split_iterator = line.split(',');
@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet. // Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_default(); let team_1 = scores
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values. // Update the values.
team_1.goals_scored += team_1_score; team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score; team_1.goals_conceded += team_2_score;
// Similarly for the second team. // Similarely for the second team.
let team_2 = scores.entry(team_2_name).or_default(); let team_2 = scores
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score; team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score; team_2.goals_conceded += team_1_score;
} }
@ -60,11 +64,9 @@ England,Spain,1,0";
fn build_scores() { fn build_scores() {
let scores = build_scores_table(RESULTS); let scores = build_scores_table(RESULTS);
assert!( assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"]
["England", "France", "Germany", "Italy", "Poland", "Spain"] .into_iter()
.into_iter() .all(|team_name| scores.contains_key(team_name)));
.all(|team_name| scores.contains_key(team_name))
);
} }
#[test] #[test]

View File

@ -1,8 +1,8 @@
// This function returns how much ice cream there is left in the fridge. // This function returns how much icecream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no ice cream is left (value 0). Return `None` if // someone eats it all, so no icecream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23. // `hour_of_day` is higher than 23.
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> { fn maybe_icecream(hour_of_day: u16) -> Option<u16> {
match hour_of_day { match hour_of_day {
0..=21 => Some(5), 0..=21 => Some(5),
22..=23 => Some(0), 22..=23 => Some(0),
@ -21,19 +21,19 @@ mod tests {
#[test] #[test]
fn raw_value() { fn raw_value() {
// Using `unwrap` is fine in a test. // Using `unwrap` is fine in a test.
let ice_creams = maybe_ice_cream(12).unwrap(); let icecreams = maybe_icecream(12).unwrap();
assert_eq!(ice_creams, 5); assert_eq!(icecreams, 5);
} }
#[test] #[test]
fn check_ice_cream() { fn check_icecream() {
assert_eq!(maybe_ice_cream(0), Some(5)); assert_eq!(maybe_icecream(0), Some(5));
assert_eq!(maybe_ice_cream(9), Some(5)); assert_eq!(maybe_icecream(9), Some(5));
assert_eq!(maybe_ice_cream(18), Some(5)); assert_eq!(maybe_icecream(18), Some(5));
assert_eq!(maybe_ice_cream(22), Some(0)); assert_eq!(maybe_icecream(22), Some(0));
assert_eq!(maybe_ice_cream(23), Some(0)); assert_eq!(maybe_icecream(23), Some(0));
assert_eq!(maybe_ice_cream(24), None); assert_eq!(maybe_icecream(24), None);
assert_eq!(maybe_ice_cream(25), None); assert_eq!(maybe_icecream(25), None);
} }
} }

View File

@ -10,7 +10,7 @@ fn main() {
// Solution 1: Matching over the `Option` (not `&Option`) but without moving // Solution 1: Matching over the `Option` (not `&Option`) but without moving
// out of the `Some` variant. // out of the `Some` variant.
match optional_point { match optional_point {
Some(ref p) => println!("Coordinates are {},{}", p.x, p.y), Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
// ^^^ added // ^^^ added
_ => panic!("No match!"), _ => panic!("No match!"),
} }
@ -18,8 +18,7 @@ fn main() {
// Solution 2: Matching over a reference (`&Option`) by added `&` before // Solution 2: Matching over a reference (`&Option`) by added `&` before
// `optional_point`. // `optional_point`.
match &optional_point { match &optional_point {
//^ added Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
Some(p) => println!("Coordinates are {},{}", p.x, p.y),
_ => panic!("No match!"), _ => panic!("No match!"),
} }

View File

@ -16,7 +16,7 @@
use std::num::ParseIntError; use std::num::ParseIntError;
#[allow(unused_variables, clippy::question_mark)] #[allow(unused_variables)]
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1; let processing_fee = 1;
let cost_per_item = 5; let cost_per_item = 5;

View File

@ -1,4 +1,4 @@
use std::cmp::Ordering; #![allow(clippy::comparison_chain)]
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
enum CreationError { enum CreationError {
@ -11,10 +11,12 @@ struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger { impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> { fn new(value: i64) -> Result<Self, CreationError> {
match value.cmp(&0) { if value == 0 {
Ordering::Less => Err(CreationError::Negative), Err(CreationError::Zero)
Ordering::Equal => Err(CreationError::Zero), } else if value < 0 {
Ordering::Greater => Ok(Self(value as u64)), Err(CreationError::Negative)
} else {
Ok(Self(value as u64))
} }
} }
} }

View File

@ -6,7 +6,7 @@
// //
// In short, this particular use case for boxes is for when you want to own a // 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 // 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<dyn Trait>` where // trait. To do so, The `Box` is declared as of type `Box<dyn Trait>` where
// `Trait` is the trait the compiler looks for on any value used in that // `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 // context. For this exercise, that context is the potential errors which
// can be returned in a `Result`. // can be returned in a `Result`.

View File

@ -29,21 +29,6 @@ impl ParsePosNonzeroError {
} }
} }
// 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`.
//
// ```
// let x: i64 = s.parse()?;
// ```
//
// Traits like `From` will be dealt with in later exercises.
impl From<ParseIntError> for ParsePosNonzeroError {
fn from(err: ParseIntError) -> Self {
ParsePosNonzeroError::ParseInt(err)
}
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64); struct PositiveNonzeroInteger(u64);

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
trait Licensed { trait Licensed {
fn licensing_info(&self) -> String { fn licensing_info(&self) -> String {
"Default license".to_string() "Default license".to_string()

View File

@ -5,7 +5,11 @@
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// ^^^^ ^^ ^^ ^^ // ^^^^ ^^ ^^ ^^
if x.len() > y.len() { x } else { y } if x.len() > y.len() {
x
} else {
y
}
} }
fn main() { fn main() {

View File

@ -1,10 +1,14 @@
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y } if x.len() > y.len() {
x
} else {
y
}
} }
fn main() { fn main() {
let string1 = String::from("long string is long"); let string1 = String::from("long string is long");
// Solution 1: You can move `strings2` out of the inner block so that it is // Solution1: You can move `strings2` out of the inner block so that it is
// not dropped before the print statement. // not dropped before the print statement.
let string2 = String::from("xyz"); let string2 = String::from("xyz");
let result; let result;
@ -21,7 +25,7 @@ fn main() {
{ {
let string2 = String::from("xyz"); let string2 = String::from("xyz");
result = longest(&string1, &string2); result = longest(&string1, &string2);
// Solution 2: You can move the print statement into the inner block so // Solution2: You can move the print statement into the inner block so
// that it is executed before `string2` is dropped. // that it is executed before `string2` is dropped.
println!("The longest string is '{result}'"); println!("The longest string is '{result}'");
// `string2` dropped here (end of the inner scope). // `string2` dropped here (end of the inner scope).

View File

@ -52,8 +52,6 @@ mod tests {
#[test] #[test]
fn test_success() { fn test_success() {
assert_eq!(divide(81, 9), Ok(9)); assert_eq!(divide(81, 9), Ok(9));
assert_eq!(divide(81, -1), Ok(-81));
assert_eq!(divide(i64::MIN, i64::MIN), Ok(1));
} }
#[test] #[test]

View File

@ -25,7 +25,6 @@ fn factorial_fold(num: u64) -> u64 {
// -> 1 * 2 is calculated, then the result 2 is multiplied by // -> 1 * 2 is calculated, then the result 2 is multiplied by
// the second element 3 so the result 6 is returned. // the second element 3 so the result 6 is returned.
// And so on… // And so on…
#[allow(clippy::unnecessary_fold)]
(2..=num).fold(1, |acc, x| acc * x) (2..=num).fold(1, |acc, x| acc * x)
} }

View File

@ -8,6 +8,7 @@ use std::rc::Rc;
#[derive(Debug)] #[derive(Debug)]
struct Sun; struct Sun;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
enum Planet { enum Planet {
Mercury(Rc<Sun>), Mercury(Rc<Sun>),
@ -63,10 +64,12 @@ mod tests {
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
saturn.details(); saturn.details();
// TODO
let uranus = Planet::Uranus(Rc::clone(&sun)); let uranus = Planet::Uranus(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
uranus.details(); uranus.details();
// TODO
let neptune = Planet::Neptune(Rc::clone(&sun)); let neptune = Planet::Neptune(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
neptune.details(); neptune.details();

View File

@ -1,5 +1,5 @@
// This program spawns multiple threads that each runs for at least 250ms, and // This program spawns multiple threads that each run for at least 250ms, and
// each thread returns how much time it took to complete. The program should // each thread returns how much time they took to complete. The program should
// wait until all the spawned threads have finished and should collect their // wait until all the spawned threads have finished and should collect their
// return values into a vector. // return values into a vector.

View File

@ -1,6 +1,7 @@
use std::{sync::mpsc, thread, time::Duration}; use std::{sync::mpsc, thread, time::Duration};
struct Queue { struct Queue {
length: u32,
first_half: Vec<u32>, first_half: Vec<u32>,
second_half: Vec<u32>, second_half: Vec<u32>,
} }
@ -8,6 +9,7 @@ struct Queue {
impl Queue { impl Queue {
fn new() -> Self { fn new() -> Self {
Self { Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5], first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10], second_half: vec![6, 7, 8, 9, 10],
} }
@ -48,15 +50,17 @@ mod tests {
fn threads3() { fn threads3() {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let queue = Queue::new(); let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx); send_tx(queue, tx);
let mut received = Vec::with_capacity(10); let mut total_received: u32 = 0;
for value in rx { for received in rx {
received.push(value); println!("Got: {received}");
total_received += 1;
} }
received.sort(); println!("Number of received values: {total_received}");
assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); assert_eq!(total_received, queue_length);
} }
} }

View File

@ -1,4 +1,4 @@
// Added the `macro_use` attribute. // Added the attribute `macro_use` attribute.
#[macro_use] #[macro_use]
mod macros { mod macros {
macro_rules! my_macro { macro_rules! my_macro {

View File

@ -1,31 +1,31 @@
use std::mem; use std::mem;
#[rustfmt::skip]
#[allow(unused_variables, unused_assignments)] #[allow(unused_variables, unused_assignments)]
fn main() { fn main() {
let my_option: Option<&str> = None; let my_option: Option<()> = None;
// `unwrap` of an `Option` after checking if it is `None` will panic. // `unwrap` of an `Option` after checking if it is `None` will panic.
// Use `if-let` instead. // Use `if-let` instead.
if let Some(value) = my_option { if let Some(value) = my_option {
println!("{value}"); println!("{value:?}");
} }
// A comma was missing. // A comma was missing.
#[rustfmt::skip]
let my_arr = &[ let my_arr = &[
-1, -2, -3, -1, -2, -3,
-4, -5, -6, -4, -5, -6,
]; ];
println!("My array! Here it is: {my_arr:?}"); println!("My array! Here it is: {:?}", my_arr);
let mut my_vec = vec![1, 2, 3, 4, 5]; let mut my_empty_vec = vec![1, 2, 3, 4, 5];
// `resize` mutates a vector instead of returning a new one. // `resize` mutates a vector instead of returning a new one.
// `resize(0, …)` clears a vector, so it is better to use `clear`. // `resize(0, …)` clears a vector, so it is better to use `clear`.
my_vec.clear(); my_empty_vec.clear();
println!("This Vec is empty, see? {my_vec:?}"); println!("This Vec is empty, see? {my_empty_vec:?}");
let mut value_a = 45; let mut value_a = 45;
let mut value_b = 66; let mut value_b = 66;
// Use `mem::swap` to correctly swap two values. // Use `mem::swap` to correctly swap two values.
mem::swap(&mut value_a, &mut value_b); mem::swap(&mut value_a, &mut value_b);
println!("value a: {value_a}; value b: {value_b}"); println!("value a: {}; value b: {}", value_a, value_b);
} }

View File

@ -2,10 +2,9 @@
// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Obtain the number of bytes (not characters) in the given argument // Obtain the number of bytes (not characters) in the given argument.
// (`.len()` returns the number of bytes in a string).
fn byte_counter<T: AsRef<str>>(arg: T) -> usize { fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
arg.as_ref().len() arg.as_ref().as_bytes().len()
} }
// Obtain the number of characters (not bytes) in the given argument. // Obtain the number of characters (not bytes) in the given argument.

View File

@ -3,4 +3,4 @@
Before you finish an exercise, its solution file will only contain an empty `main` function. Before you finish an exercise, its solution file will only contain an empty `main` function.
The content of this file will be automatically replaced by the actual solution once you finish the exercise. The content of this file will be automatically replaced by the actual solution once you finish the exercise.
Note that these solutions are often only _one possibility_ to solve an exercise. Note that these solution are often only _one possibility_ to solve an exercise.

View File

@ -62,8 +62,8 @@ mod tests {
// Import `transformer`. // Import `transformer`.
use super::my_module::transformer; use super::my_module::transformer;
use super::Command;
use super::my_module::transformer_iter; use super::my_module::transformer_iter;
use super::Command;
#[test] #[test]
fn it_works() { fn it_works() {

View File

@ -1,39 +1,32 @@
use anyhow::{Context, Error, Result, bail}; use anyhow::{bail, Context, Error, Result};
use crossterm::{QueueableCommand, cursor, terminal};
use std::{ use std::{
collections::HashSet, fs::{self, File},
env, io::{Read, StdoutLock, Write},
fs::{File, OpenOptions}, path::Path,
io::{Read, Seek, StdoutLock, Write},
path::{MAIN_SEPARATOR_STR, Path},
process::{Command, Stdio}, process::{Command, Stdio},
sync::{
atomic::{AtomicUsize, Ordering::Relaxed},
mpsc,
},
thread, thread,
}; };
use crate::{ use crate::{
clear_terminal, clear_terminal,
cmd::CmdRunner, cmd::CmdRunner,
collections::hash_set_with_capacity,
embedded::EMBEDDED_FILES, embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise}, exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo, info_file::ExerciseInfo,
term::{self, CheckProgressVisualizer},
}; };
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const DEFAULT_CHECK_PARALLELISM: usize = 8; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
// All exercises are done. // All exercises are done.
AllDone, AllDone,
// The current exercise failed and is still pending.
CurrentPending,
// A new exercise is now pending. // A new exercise is now pending.
NewPending, NewPending,
// The current exercise is still pending.
CurrentPending,
} }
pub enum StateFileStatus { pub enum StateFileStatus {
@ -41,46 +34,72 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
#[derive(Clone, Copy)]
pub enum CheckProgress {
None,
Checking,
Done,
Pending,
}
pub struct AppState { pub struct AppState {
current_exercise_ind: usize, current_exercise_ind: usize,
exercises: Vec<Exercise>, exercises: Vec<Exercise>,
// Caches the number of done exercises to avoid iterating over all exercises every time. // Caches the number of done exercises to avoid iterating over all exercises every time.
n_done: u16, n_done: u16,
final_message: String, final_message: String,
state_file: File,
// Preallocated buffer for reading and writing the state file. // Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>, file_buf: Vec<u8>,
official_exercises: bool, official_exercises: bool,
cmd_runner: CmdRunner, cmd_runner: CmdRunner,
emit_file_links: bool,
} }
impl AppState { impl AppState {
// Update the app state from the state file.
fn update_from_file(&mut self) -> StateFileStatus {
self.file_buf.clear();
self.n_done = 0;
if File::open(STATE_FILE_NAME)
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
.is_err()
{
return StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
return StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
return StateFileStatus::NotRead;
}
let mut done_exercises = hash_set_with_capacity(self.exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
self.n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
self.current_exercise_ind = ind;
}
}
StateFileStatus::Read
}
pub fn new( pub fn new(
exercise_infos: Vec<ExerciseInfo>, exercise_infos: Vec<ExerciseInfo>,
final_message: String, final_message: String,
) -> Result<(Self, StateFileStatus)> { ) -> Result<(Self, StateFileStatus)> {
let cmd_runner = CmdRunner::build()?; let cmd_runner = CmdRunner::build()?;
let mut state_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(STATE_FILE_NAME)
.with_context(|| {
format!("Failed to open or create the state file {STATE_FILE_NAME}")
})?;
let dir_canonical_path = term::canonicalize("exercises"); let exercises = exercise_infos
let mut exercises = exercise_infos
.into_iter() .into_iter()
.map(|exercise_info| { .map(|exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`. // Leaking to be able to borrow in the watch mode `Table`.
@ -91,99 +110,31 @@ impl AppState {
let dir = exercise_info.dir.map(|dir| &*dir.leak()); let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.leak().trim_ascii(); let hint = exercise_info.hint.leak().trim_ascii();
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path;
if let Some(dir) = dir {
canonical_path = String::with_capacity(
2 + dir_canonical_path.len() + dir.len() + 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.push_str(dir_canonical_path);
}
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(name);
canonical_path.push_str(".rs");
canonical_path
});
Exercise { Exercise {
dir, dir,
name, name,
path, path,
canonical_path,
test: exercise_info.test, test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy, strict_clippy: exercise_info.strict_clippy,
hint, hint,
// Updated below. // Updated in `Self::update_from_file`.
done: false, done: false,
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut current_exercise_ind = 0; let mut slf = Self {
let mut n_done = 0; current_exercise_ind: 0,
let mut file_buf = Vec::with_capacity(2048);
let state_file_status = 'block: {
if state_file.read_to_end(&mut file_buf).is_err() {
break 'block StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
break 'block StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
break 'block StateFileStatus::NotRead;
}
let mut done_exercises = HashSet::with_capacity(exercises.len());
for done_exercise_name in lines {
if done_exercise_name.is_empty() {
break;
}
done_exercises.insert(done_exercise_name);
}
for (ind, exercise) in exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
current_exercise_ind = ind;
}
}
StateFileStatus::Read
};
file_buf.clear();
file_buf.extend_from_slice(STATE_FILE_HEADER);
let slf = Self {
current_exercise_ind,
exercises, exercises,
n_done, n_done: 0,
final_message, final_message,
state_file, file_buf: Vec::with_capacity(2048),
file_buf,
official_exercises: !Path::new("info.toml").exists(), official_exercises: !Path::new("info.toml").exists(),
cmd_runner, cmd_runner,
// VS Code has its own file link handling
emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"),
}; };
let state_file_status = slf.update_from_file();
Ok((slf, state_file_status)) Ok((slf, state_file_status))
} }
@ -202,11 +153,6 @@ impl AppState {
self.n_done self.n_done
} }
#[inline]
pub fn n_pending(&self) -> u16 {
self.exercises.len() as u16 - self.n_done
}
#[inline] #[inline]
pub fn current_exercise(&self) -> &Exercise { pub fn current_exercise(&self) -> &Exercise {
&self.exercises[self.current_exercise_ind] &self.exercises[self.current_exercise_ind]
@ -217,11 +163,6 @@ impl AppState {
&self.cmd_runner &self.cmd_runner
} }
#[inline]
pub fn emit_file_links(&self) -> bool {
self.emit_file_links
}
// Write the state file. // Write the state file.
// The file's format is very simple: // The file's format is very simple:
// - The first line is a comment. // - The first line is a comment.
@ -231,8 +172,10 @@ impl AppState {
// - The fourth line is an empty line. // - The fourth line is an empty line.
// - All remaining lines are the names of done exercises. // - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> { fn write(&mut self) -> Result<()> {
self.file_buf.truncate(STATE_FILE_HEADER.len()); self.file_buf.clear();
self.file_buf
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
self.file_buf self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes()); .extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.push(b'\n'); self.file_buf.push(b'\n');
@ -244,14 +187,7 @@ impl AppState {
} }
} }
self.state_file fs::write(STATE_FILE_NAME, &self.file_buf)
.rewind()
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
self.state_file
.set_len(0)
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
self.state_file
.write_all(&self.file_buf)
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(()) Ok(())
@ -283,31 +219,15 @@ impl AppState {
self.write() self.write()
} }
// Set the status of an exercise without saving. Returns `true` if the pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
// status actually changed (and thus needs saving later).
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
let exercise = self let exercise = self
.exercises .exercises
.get_mut(exercise_ind) .get_mut(exercise_ind)
.context(BAD_INDEX_ERR)?; .context(BAD_INDEX_ERR)?;
if exercise.done == done { if exercise.done {
return Ok(false); exercise.done = false;
}
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1; self.n_done -= 1;
}
Ok(true)
}
// Set the status of an exercise to "pending" and save.
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
if self.set_status(exercise_ind, false)? {
self.write()?; self.write()?;
} }
@ -315,7 +235,7 @@ impl AppState {
} }
// Official exercises: Dump the original file from the binary. // Official exercises: Dump the original file from the binary.
// Community exercises: Reset the exercise file with `git stash`. // Third-party exercises: Reset the exercise file with `git stash`.
fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> { fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> {
if self.official_exercises { if self.official_exercises {
return EMBEDDED_FILES return EMBEDDED_FILES
@ -351,7 +271,6 @@ impl AppState {
Ok(exercise.path) Ok(exercise.path)
} }
// Reset the exercise by index and return its name.
pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> {
if exercise_ind >= self.exercises.len() { if exercise_ind >= self.exercises.len() {
bail!(BAD_INDEX_ERR); bail!(BAD_INDEX_ERR);
@ -361,31 +280,34 @@ impl AppState {
let exercise = &self.exercises[exercise_ind]; let exercise = &self.exercises[exercise_ind];
self.reset(exercise_ind, exercise.path)?; self.reset(exercise_ind, exercise.path)?;
Ok(exercise.name) Ok(exercise.path)
} }
// Return the index of the next pending exercise or `None` if all exercises are done. // Return the index of the next pending exercise or `None` if all exercises are done.
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
let next_ind = self.current_exercise_ind + 1; if self.current_exercise_ind == self.exercises.len() - 1 {
self.exercises // The last exercise is done.
// If the exercise done isn't the last, search for pending exercises after it. // Search for exercises not done from the start.
.get(next_ind..) return self.exercises[..self.current_exercise_ind]
.and_then(|later_exercises| { .iter()
later_exercises .position(|exercise| !exercise.done);
.iter() }
.position(|exercise| !exercise.done)
.map(|ind| next_ind + ind) // The done exercise isn't the last one.
}) // Search for a pending exercise after the current one and then from the start.
// Search from the start. match self.exercises[self.current_exercise_ind + 1..]
.or_else(|| { .iter()
self.exercises[..self.current_exercise_ind] .position(|exercise| !exercise.done)
.iter() {
.position(|exercise| !exercise.done) Some(ind) => Some(self.current_exercise_ind + 1 + ind),
}) None => self.exercises[..self.current_exercise_ind]
.iter()
.position(|exercise| !exercise.done),
}
} }
/// Official exercises: Dump the solution file from the binary and return its path. /// Official exercises: Dump the solution file form the binary and return its path.
/// Community exercises: Check if a solution file exists and return its path in that case. /// Third-party exercises: Check if a solution file exists and return its path in that case.
pub fn current_solution_path(&self) -> Result<Option<String>> { pub fn current_solution_path(&self) -> Result<Option<String>> {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
return Ok(None); return Ok(None);
@ -398,135 +320,24 @@ impl AppState {
.write_solution_to_disk(self.current_exercise_ind, current_exercise.name) .write_solution_to_disk(self.current_exercise_ind, current_exercise.name)
.map(Some) .map(Some)
} else { } else {
let sol_path = current_exercise.sol_path(); let solution_path = if let Some(dir) = current_exercise.dir {
format!("solutions/{dir}/{}.rs", current_exercise.name)
} else {
format!("solutions/{}.rs", current_exercise.name)
};
if Path::new(&sol_path).exists() { if Path::new(&solution_path).exists() {
return Ok(Some(sol_path)); return Ok(Some(solution_path));
} }
Ok(None) Ok(None)
} }
} }
fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
let next_exercise_ind = AtomicUsize::new(0);
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
thread::scope(|s| {
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
let n_threads = thread::available_parallelism()
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
for _ in 0..n_threads {
let exercise_progress_sender = exercise_progress_sender.clone();
let next_exercise_ind = &next_exercise_ind;
let slf = &self;
thread::Builder::new()
.spawn_scoped(s, move || {
loop {
let exercise_ind = next_exercise_ind.fetch_add(1, Relaxed);
let Some(exercise) = slf.exercises.get(exercise_ind) else {
// No more exercises.
break;
};
if exercise_progress_sender
.send((exercise_ind, CheckProgress::Checking))
.is_err()
{
break;
};
let success = exercise.run_exercise(None, &slf.cmd_runner);
let progress = match success {
Ok(true) => CheckProgress::Done,
Ok(false) => CheckProgress::Pending,
Err(_) => CheckProgress::None,
};
if exercise_progress_sender
.send((exercise_ind, progress))
.is_err()
{
break;
}
}
})
.context("Failed to spawn a thread to check all exercises")?;
}
// Drop this sender to detect when the last thread is done.
drop(exercise_progress_sender);
while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
progresses[exercise_ind] = progress;
progress_visualizer.update(&progresses)?;
}
Ok::<_, Error>(())
})?;
let mut first_pending_exercise_ind = None;
for exercise_ind in 0..progresses.len() {
match progresses[exercise_ind] {
CheckProgress::Done => {
self.set_status(exercise_ind, true)?;
}
CheckProgress::Pending => {
self.set_status(exercise_ind, false)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
CheckProgress::None | CheckProgress::Checking => {
// If we got an error while checking all exercises in parallel,
// it could be because we exceeded the limit of open file descriptors.
// Therefore, try running exercises with errors sequentially.
progresses[exercise_ind] = CheckProgress::Checking;
progress_visualizer.update(&progresses)?;
let exercise = &self.exercises[exercise_ind];
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if success {
progresses[exercise_ind] = CheckProgress::Done;
} else {
progresses[exercise_ind] = CheckProgress::Pending;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
self.set_status(exercise_ind, success)?;
progress_visualizer.update(&progresses)?;
}
}
}
self.write()?;
Ok(first_pending_exercise_ind)
}
// Return the exercise index of the first pending exercise found.
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.queue(cursor::Hide)?;
let res = self.check_all_exercises_impl(stdout);
stdout.queue(cursor::Show)?;
res
}
/// Mark the current exercise as done and move on to the next pending exercise if one exists. /// Mark the current exercise as done and move on to the next pending exercise if one exists.
/// If all exercises are marked as done, run all of them to make sure that they are actually /// If all exercises are marked as done, run all of them to make sure that they are actually
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it. /// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
pub fn done_current_exercise<const CLEAR_BEFORE_FINAL_CHECK: bool>( pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
&mut self,
stdout: &mut StdoutLock,
) -> Result<ExercisesProgress> {
let exercise = &mut self.exercises[self.current_exercise_ind]; let exercise = &mut self.exercises[self.current_exercise_ind];
if !exercise.done { if !exercise.done {
exercise.done = true; exercise.done = true;
@ -538,39 +349,69 @@ impl AppState {
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
if CLEAR_BEFORE_FINAL_CHECK { writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
clear_terminal(stdout)?;
} else {
stdout.write_all(b"\n")?;
}
if let Some(first_pending_exercise_ind) = self.check_all_exercises(stdout)? { let n_exercises = self.exercises.len();
self.set_current_exercise_ind(first_pending_exercise_ind)?;
let pending_exercise_ind = thread::scope(|s| {
let handles = self
.exercises
.iter_mut()
.map(|exercise| {
s.spawn(|| {
let success = exercise.run_exercise(None, &self.cmd_runner)?;
exercise.done = success;
Ok::<_, Error>(success)
})
})
.collect::<Vec<_>>();
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?;
writer.flush()?;
let success = handle.join().unwrap()?;
if !success {
writer.write_all(b"\n\n")?;
return Ok(Some(exercise_ind));
}
}
Ok::<_, Error>(None)
})?;
if let Some(pending_exercise_ind) = pending_exercise_ind {
self.current_exercise_ind = pending_exercise_ind;
self.n_done = self
.exercises
.iter()
.filter(|exercise| exercise.done)
.count() as u16;
self.write()?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
self.render_final_message(stdout)?; // Write that the last exercise is done.
self.write()?;
Ok(ExercisesProgress::AllDone) clear_terminal(writer)?;
} writer.write_all(FENISH_LINE.as_bytes())?;
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?;
let final_message = self.final_message.trim_ascii(); let final_message = self.final_message.trim_ascii();
if !final_message.is_empty() { if !final_message.is_empty() {
stdout.write_all(final_message.as_bytes())?; writer.write_all(final_message.as_bytes())?;
stdout.write_all(b"\n")?; writer.write_all(b"\n")?;
} }
Ok(()) Ok(ExercisesProgress::AllDone)
} }
} }
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done.
";
const FENISH_LINE: &str = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |
+-------------------------- ------------------------+ +-------------------------- ------------------------+
@ -602,7 +443,6 @@ mod tests {
dir: None, dir: None,
name: "0", name: "0",
path: "exercises/0.rs", path: "exercises/0.rs",
canonical_path: None,
test: false, test: false,
strict_clippy: false, strict_clippy: false,
hint: "", hint: "",
@ -617,11 +457,9 @@ mod tests {
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
n_done: 0, n_done: 0,
final_message: String::new(), final_message: String::new(),
state_file: tempfile::tempfile().unwrap(),
file_buf: Vec::new(), file_buf: Vec::new(),
official_exercises: true, official_exercises: true,
cmd_runner: CmdRunner::build().unwrap(), cmd_runner: CmdRunner::build().unwrap(),
emit_file_links: true,
}; };
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| { let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {

View File

@ -1,7 +1,7 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::path::Path; use std::path::Path;
use crate::{exercise::RunnableExercise, info_file::ExerciseInfo}; use crate::info_file::ExerciseInfo;
/// Initial capacity of the bins buffer. /// Initial capacity of the bins buffer.
pub const BINS_BUFFER_CAPACITY: usize = 1 << 14; pub const BINS_BUFFER_CAPACITY: usize = 1 << 14;
@ -74,13 +74,13 @@ pub fn updated_cargo_toml(
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY); let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY);
updated_cargo_toml.extend_from_slice(&current_cargo_toml.as_bytes()[..bins_start_ind]); updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes());
append_bins( append_bins(
&mut updated_cargo_toml, &mut updated_cargo_toml,
exercise_infos, exercise_infos,
exercise_path_prefix, exercise_path_prefix,
); );
updated_cargo_toml.extend_from_slice(&current_cargo_toml.as_bytes()[bins_end_ind..]); updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes());
Ok(updated_cargo_toml) Ok(updated_cargo_toml)
} }
@ -134,14 +134,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
updated_cargo_toml( updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(),
&exercise_infos,
"abc\n\
bin = [xxx]\n\
123",
b"../"
)
.unwrap(),
br#"abc br#"abc
bin = [ bin = [
{ name = "1", path = "../exercises/1.rs" }, { name = "1", path = "../exercises/1.rs" },

View File

@ -1,7 +1,7 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
io::{Read, pipe}, io::Read,
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
@ -17,7 +17,7 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) ->
}; };
let mut handle = if let Some(output) = output { let mut handle = if let Some(output) = output {
let (mut reader, writer) = pipe().with_context(|| { let (mut reader, writer) = os_pipe::pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{description}``") format!("Failed to create a pipe to run the command `{description}``")
})?; })?;
@ -74,14 +74,12 @@ impl CmdRunner {
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?"); bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
} }
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout) let target_dir = serde_json::de::from_slice::<CargoMetadata>(&metadata_output.stdout)
.context( .context(
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`", "Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
)?; )?.target_directory;
Ok(Self { Ok(Self { target_dir })
target_dir: metadata.target_directory,
})
} }
pub fn cargo<'out>( pub fn cargo<'out>(
@ -125,7 +123,7 @@ pub struct CargoSubcommand<'out> {
output: Option<&'out mut Vec<u8>>, output: Option<&'out mut Vec<u8>>,
} }
impl CargoSubcommand<'_> { impl<'out> CargoSubcommand<'out> {
#[inline] #[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where where

10
src/collections.rs Normal file
View File

@ -0,0 +1,10 @@
use ahash::AHasher;
use std::hash::BuildHasherDefault;
/// DOS attacks aren't a concern for Rustlings. Therefore, we use `ahash` with fixed seeds.
pub type HashSet<T> = std::collections::HashSet<T, BuildHasherDefault<AHasher>>;
#[inline]
pub fn hash_set_with_capacity<T>(capacity: usize) -> HashSet<T> {
HashSet::with_capacity_and_hasher(capacity, BuildHasherDefault::<AHasher>::default())
}

View File

@ -1,4 +1,4 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use clap::Subcommand; use clap::Subcommand;
use std::path::PathBuf; use std::path::PathBuf;
@ -8,7 +8,7 @@ mod update;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum DevCommands { pub enum DevCommands {
/// Create a new project for community exercises /// Create a new project for third-party Rustlings exercises
New { New {
/// The path to create the project in /// The path to create the project in
path: PathBuf, path: PathBuf,

View File

@ -1,8 +1,7 @@
use anyhow::{Context, Error, Result, anyhow, bail}; use anyhow::{anyhow, bail, Context, Error, Result};
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::HashSet, fs::{self, read_dir, OpenOptions},
fs::{self, OpenOptions, read_dir},
io::{self, Read, Write}, io::{self, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Stdio}, process::{Command, Stdio},
@ -10,17 +9,14 @@ use std::{
}; };
use crate::{ use crate::{
CURRENT_FORMAT_VERSION, cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind},
cmd::CmdRunner, cmd::CmdRunner,
exercise::{OUTPUT_CAPACITY, RunnableExercise}, collections::{hash_set_with_capacity, HashSet},
exercise::{RunnableExercise, OUTPUT_CAPACITY},
info_file::{ExerciseInfo, InfoFile}, info_file::{ExerciseInfo, InfoFile},
term::ProgressCounter, CURRENT_FORMAT_VERSION,
}; };
const MAX_N_EXERCISES: usize = 999;
const MAX_EXERCISE_NAME_LEN: usize = 32;
// Find a char that isn't allowed in the exercise's `name` or `dir`. // Find a char that isn't allowed in the exercise's `name` or `dir`.
fn forbidden_char(input: &str) -> Option<char> { fn forbidden_char(input: &str) -> Option<char> {
input.chars().find(|c| !c.is_alphanumeric() && *c != '_') input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
@ -43,14 +39,10 @@ fn check_cargo_toml(
if old_bins != new_bins { if old_bins != new_bins {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
bail!( bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
"The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again"
);
} }
bail!( bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
"The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"
);
} }
Ok(()) Ok(())
@ -58,8 +50,8 @@ fn check_cargo_toml(
// Check the info of all exercises and return their paths in a set. // Check the info of all exercises and return their paths in a set.
fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> { fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
let mut names = HashSet::with_capacity(info_file.exercises.len()); let mut names = hash_set_with_capacity(info_file.exercises.len());
let mut paths = HashSet::with_capacity(info_file.exercises.len()); let mut paths = hash_set_with_capacity(info_file.exercises.len());
let mut file_buf = String::with_capacity(1 << 14); let mut file_buf = String::with_capacity(1 << 14);
for exercise_info in &info_file.exercises { for exercise_info in &info_file.exercises {
@ -67,11 +59,6 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
if name.is_empty() { if name.is_empty() {
bail!("Found an empty exercise name in `info.toml`"); bail!("Found an empty exercise name in `info.toml`");
} }
if name.len() > MAX_EXERCISE_NAME_LEN {
bail!(
"The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN}"
);
}
if let Some(c) = forbidden_char(name) { if let Some(c) = forbidden_char(name) {
bail!("Char `{c}` in the exercise name `{name}` is not allowed"); bail!("Char `{c}` in the exercise name `{name}` is not allowed");
} }
@ -86,9 +73,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
} }
if exercise_info.hint.trim_ascii().is_empty() { if exercise_info.hint.trim_ascii().is_empty() {
bail!( bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
"The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise"
);
} }
if !names.insert(name) { if !names.insert(name) {
@ -105,30 +90,15 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
.with_context(|| format!("Failed to read the file {path}"))?; .with_context(|| format!("Failed to read the file {path}"))?;
if !file_buf.contains("fn main()") { if !file_buf.contains("fn main()") {
bail!( bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors");
"The `main` function is missing in the file `{path}`.\n\
Create at least an empty `main` function to avoid language server errors"
);
} }
if !file_buf.contains("// TODO") { if !file_buf.contains("// TODO") {
bail!( bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
"Didn't find any `// TODO` comment in the file `{path}`.\n\
You need to have at least one such comment to guide the user."
);
} }
let contains_tests = file_buf.contains("#[test]\n"); if !exercise_info.test && file_buf.contains("#[test]") {
if exercise_info.test { bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
if !contains_tests {
bail!(
"The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file"
);
}
} else if contains_tests {
bail!(
"The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"
);
} }
file_buf.clear(); file_buf.clear();
@ -144,10 +114,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
// Only one level of directory nesting is allowed. // Only one level of directory nesting is allowed.
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> { fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
let unexpected_file = |path: &Path| { let unexpected_file = |path: &Path| {
anyhow!( anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display())
"Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
path.display()
)
}; };
for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? { for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? {
@ -176,10 +143,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
let path = entry.path(); let path = entry.path();
if !entry.file_type().unwrap().is_file() { if !entry.file_type().unwrap().is_file() {
bail!( bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display());
"Found `{}` but expected only files. Only one level of exercise nesting is allowed",
path.display()
);
} }
let file_name = path.file_name().unwrap(); let file_name = path.file_name().unwrap();
@ -196,79 +160,61 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
Ok(()) Ok(())
} }
fn check_exercises_unsolved( fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
info_file: &'static InfoFile, println!(
cmd_runner: &'static CmdRunner, "Running all exercises to check that they aren't already solved. This may take a while…\n",
) -> Result<()> { );
let mut stdout = io::stdout().lock(); thread::scope(|s| {
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?; let handles = info_file
.exercises
.iter()
.filter_map(|exercise_info| {
if exercise_info.skip_check_unsolved {
return None;
}
let handles = info_file Some((
.exercises exercise_info.name.as_str(),
.iter() s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
.filter_map(|exercise_info| { ))
if exercise_info.skip_check_unsolved { })
return None; .collect::<Vec<_>>();
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
};
match result {
Ok(true) => bail!(
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
),
Ok(false) => (),
Err(e) => return Err(e),
} }
Some(
thread::Builder::new()
.spawn(|| exercise_info.run_exercise(None, cmd_runner))
.map(|handle| (exercise_info.name.as_str(), handle)),
)
})
.collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check if an exercise is already solved")?;
let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?;
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exercise {exercise_name}");
};
match result {
Ok(true) => {
bail!(
"The exercise {exercise_name} is already solved.\n\
{SKIP_CHECK_UNSOLVED_HINT}",
)
}
Ok(false) => (),
Err(e) => return Err(e),
} }
progress_counter.increment()?; Ok(())
} })
Ok(())
} }
fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> { fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
Ordering::Less => bail!( Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
"`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\n\ Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
Please migrate to the latest format version"
),
Ordering::Greater => bail!(
"`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\n\
Try updating the Rustlings program"
),
Ordering::Equal => (), Ordering::Equal => (),
} }
let handle = thread::Builder::new()
.spawn(move || check_exercises_unsolved(info_file, cmd_runner))
.context("Failed to spawn a thread to check if any exercise is already solved")?;
let info_file_paths = check_info_file_exercises(info_file)?; let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?; let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths));
check_exercises_unsolved(info_file, cmd_runner)?;
handle.join().unwrap() handle.join().unwrap()
} }
enum SolutionCheck { enum SolutionCheck {
Success { sol_path: String }, Success { sol_path: String },
MissingRequired,
MissingOptional, MissingOptional,
RunFailure { output: Vec<u8> }, RunFailure { output: Vec<u8> },
Err(Error), Err(Error),
@ -276,119 +222,103 @@ enum SolutionCheck {
fn check_solutions( fn check_solutions(
require_solutions: bool, require_solutions: bool,
info_file: &'static InfoFile, info_file: &InfoFile,
cmd_runner: &'static CmdRunner, cmd_runner: &CmdRunner,
) -> Result<()> { ) -> Result<()> {
let mut stdout = io::stdout().lock(); println!("Running all solutions. This may take a while…\n");
stdout.write_all(b"Running all solutions...\n")?; thread::scope(|s| {
let handles = info_file
.exercises
.iter()
.map(|exercise_info| {
s.spawn(|| {
let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() {
if require_solutions {
return SolutionCheck::MissingRequired;
}
let handles = info_file return SolutionCheck::MissingOptional;
.exercises
.iter()
.map(|exercise_info| {
thread::Builder::new().spawn(move || {
let sol_path = exercise_info.sol_path();
if !Path::new(&sol_path).exists() {
if require_solutions {
return SolutionCheck::Err(anyhow!(
"The solution of the exercise {} is missing",
exercise_info.name,
));
} }
return SolutionCheck::MissingOptional; let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
} match exercise_info.run_solution(Some(&mut output), cmd_runner) {
Ok(true) => SolutionCheck::Success { sol_path },
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); Ok(false) => SolutionCheck::RunFailure { output },
match exercise_info.run_solution(Some(&mut output), cmd_runner) { Err(e) => SolutionCheck::Err(e),
Ok(true) => SolutionCheck::Success { sol_path }, }
Ok(false) => SolutionCheck::RunFailure { output }, })
Err(e) => SolutionCheck::Err(e),
}
}) })
}) .collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()
.context("Failed to spawn a thread to check a solution")?;
let mut sol_paths = HashSet::with_capacity(info_file.exercises.len()); let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt"); let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd fmt_cmd
.arg("--check") .arg("--check")
.arg("--edition") .arg("--edition")
.arg("2024") .arg("2021")
.arg("--color") .arg("--color")
.arg("always") .arg("always")
.stdin(Stdio::null()); .stdin(Stdio::null());
let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?; for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else {
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else {
bail!(
"Panic while trying to run the solution of the exercise {}",
exercise_info.name,
);
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
drop(progress_counter);
stdout.write_all(&output)?;
bail!( bail!(
"Running the solution of the exercise {} failed with the error above", "Panic while trying to run the solution of the exericse {}",
exercise_info.name, exercise_info.name,
); );
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
SolutionCheck::MissingRequired => {
bail!(
"The solution of the exercise {} is missing",
exercise_info.name,
);
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
io::stderr().lock().write_all(&output)?;
bail!(
"Running the solution of the exercise {} failed with the error above",
exercise_info.name,
);
}
SolutionCheck::Err(e) => return Err(e),
} }
SolutionCheck::Err(e) => return Err(e),
} }
progress_counter.increment()?; let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths));
}
let n_solutions = sol_paths.len(); if !fmt_cmd
let handle = thread::Builder::new()
.spawn(move || check_unexpected_files("solutions", &sol_paths))
.context(
"Failed to spawn a thread to check for unexpected files in the solutions directory",
)?;
if n_solutions > 0
&& !fmt_cmd
.status() .status()
.context("Failed to run `rustfmt` on all solution files")? .context("Failed to run `rustfmt` on all solution files")?
.success() .success()
{ {
bail!("Some solutions aren't formatted. Run `rustfmt` on them"); bail!("Some solutions aren't formatted. Run `rustfmt` on them");
} }
handle.join().unwrap() handle.join().unwrap()
})
} }
pub fn check(require_solutions: bool) -> Result<()> { pub fn check(require_solutions: bool) -> Result<()> {
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
if info_file.exercises.len() > MAX_N_EXERCISES {
bail!("The maximum number of exercises is {MAX_N_EXERCISES}");
}
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// A hack to make `cargo dev check` work when developing Rustlings. // A hack to make `cargo run -- dev check` work when developing Rustlings.
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
} else { } else {
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
} }
// Leaking is fine since they are used until the end of the program. let cmd_runner = CmdRunner::build()?;
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?)); check_exercises(&info_file, &cmd_runner)?;
let info_file = Box::leak(Box::new(info_file)); check_solutions(require_solutions, &info_file, &cmd_runner)?;
check_exercises(info_file, cmd_runner)?;
check_solutions(require_solutions, info_file, cmd_runner)?;
println!("Everything looks fine!"); println!("Everything looks fine!");

View File

@ -1,4 +1,4 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use std::{ use std::{
env::set_current_dir, env::set_current_dir,
fs::{self, create_dir}, fs::{self, create_dir},
@ -6,7 +6,7 @@ use std::{
process::Command, process::Command,
}; };
use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML}; use crate::CURRENT_FORMAT_VERSION;
// Create a directory relative to the current directory and print its path. // Create a directory relative to the current directory and print its path.
fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> {
@ -55,17 +55,13 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
write_rel_file( write_rel_file(
"info.toml", "info.toml",
&dir_path_str, &dir_path_str,
format!( format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"),
"{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"
),
)?; )?;
write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?; write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?;
write_rel_file("README.md", &dir_path_str, README)?; write_rel_file("README.md", &dir_path_str, README)?;
write_rel_file("rust-analyzer.toml", &dir_path_str, RUST_ANALYZER_TOML)?;
create_rel_dir(".vscode", &dir_path_str)?; create_rel_dir(".vscode", &dir_path_str)?;
write_rel_file( write_rel_file(
".vscode/extensions.json", ".vscode/extensions.json",
@ -78,17 +74,18 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
Ok(()) Ok(())
} }
pub const GITIGNORE: &[u8] = b"Cargo.lock pub const GITIGNORE: &[u8] = b".rustlings-state.txt
Cargo.lock
target/ target/
.vscode/ .vscode/
!.vscode/extensions.json !.vscode/extensions.json
"; ";
const INFO_FILE_BEFORE_FORMAT_VERSION: &str = const INFO_FILE_BEFORE_FORMAT_VERSION: &str =
"# The format version is an indicator of the compatibility of community exercises with the "# The format version is an indicator of the compatibility of third-party exercises with the
# Rustlings program. # Rustlings program.
# The format version is not the same as the version of the Rustlings program. # The format version is not the same as the version of the Rustlings program.
# In case Rustlings makes an unavoidable breaking change to the expected format of community # In case Rustlings makes an unavoidable breaking change to the expected format of third-party
# exercises, you would need to raise this version and adapt to the new format. # exercises, you would need to raise this version and adapt to the new format.
# Otherwise, the newest version of the Rustlings program won't be able to run these exercises. # Otherwise, the newest version of the Rustlings program won't be able to run these exercises.
format_version = "; format_version = ";
@ -96,7 +93,7 @@ format_version = ";
const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#" const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#"
# Optional multi-line message to be shown to users when just starting with the exercises. # Optional multi-line message to be shown to users when just starting with the exercises.
welcome_message = """Welcome to these community Rustlings exercises.""" welcome_message = """Welcome to these third-party Rustlings exercises."""
# Optional multi-line message to be shown to users after finishing all exercises. # Optional multi-line message to be shown to users after finishing all exercises.
final_message = """We hope that you found the exercises helpful :D""" final_message = """We hope that you found the exercises helpful :D"""
@ -131,7 +128,7 @@ bin = []
[package] [package]
name = "exercises" name = "exercises"
edition = "2024" edition = "2021"
# Don't publish the exercises on crates.io! # Don't publish the exercises on crates.io!
publish = false publish = false
@ -140,7 +137,7 @@ publish = false
const README: &str = "# Rustlings 🦀 const README: &str = "# Rustlings 🦀
Welcome to these community Rustlings exercises 😃 Welcome to these third-party Rustlings exercises 😃
First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅ First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) ✅

View File

@ -28,7 +28,7 @@ pub fn update() -> Result<()> {
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// A hack to make `cargo dev update` work when developing Rustlings. // A hack to make `cargo run -- dev update` work when developing Rustlings.
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../") update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
.context("Failed to update the file `dev/Cargo.toml`")?; .context("Failed to update the file `dev/Cargo.toml`")?;

View File

@ -1,7 +1,7 @@
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use std::{ use std::{
fs::{self, create_dir}, fs::{create_dir, OpenOptions},
io, io::{self, Write},
}; };
use crate::info_file::ExerciseInfo; use crate::info_file::ExerciseInfo;
@ -9,6 +9,29 @@ use crate::info_file::ExerciseInfo;
/// Contains all embedded files. /// Contains all embedded files.
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
#[derive(Clone, Copy)]
pub enum WriteStrategy {
IfNotExists,
Overwrite,
}
impl WriteStrategy {
fn write(self, path: &str, content: &[u8]) -> Result<()> {
let file = match self {
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
Self::Overwrite => OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path),
};
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
.write_all(content)
.with_context(|| format!("Failed to write the file {path}"))
}
}
// Files related to one exercise. // Files related to one exercise.
struct ExerciseFiles { struct ExerciseFiles {
// The content of the exercise file. // The content of the exercise file.
@ -19,16 +42,6 @@ struct ExerciseFiles {
dir_ind: usize, dir_ind: usize,
} }
fn create_dir_if_not_exists(path: &str) -> Result<()> {
if let Err(e) = create_dir(path)
&& e.kind() != io::ErrorKind::AlreadyExists
{
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
}
Ok(())
}
// A directory in the `exercises/` directory. // A directory in the `exercises/` directory.
pub struct ExerciseDir { pub struct ExerciseDir {
pub name: &'static str, pub name: &'static str,
@ -42,13 +55,21 @@ impl ExerciseDir {
let mut dir_path = String::with_capacity(20 + self.name.len()); let mut dir_path = String::with_capacity(20 + self.name.len());
dir_path.push_str("exercises/"); dir_path.push_str("exercises/");
dir_path.push_str(self.name); dir_path.push_str(self.name);
create_dir_if_not_exists(&dir_path)?;
if let Err(e) = create_dir(&dir_path) {
if e.kind() == io::ErrorKind::AlreadyExists {
return Ok(());
}
return Err(
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
);
}
let mut readme_path = dir_path; let mut readme_path = dir_path;
readme_path.push_str("/README.md"); readme_path.push_str("/README.md");
fs::write(&readme_path, self.readme) WriteStrategy::Overwrite.write(&readme_path, self.readme)
.with_context(|| format!("Failed to write the file {readme_path}"))
} }
} }
@ -65,31 +86,17 @@ impl EmbeddedFiles {
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
create_dir("exercises").context("Failed to create the directory `exercises`")?; create_dir("exercises").context("Failed to create the directory `exercises`")?;
fs::write( WriteStrategy::IfNotExists.write(
"exercises/README.md", "exercises/README.md",
include_bytes!("../exercises/README.md"), include_bytes!("../exercises/README.md"),
) )?;
.context("Failed to write the file exercises/README.md")?;
for dir in self.exercise_dirs { for dir in self.exercise_dirs {
dir.init_on_disk()?; dir.init_on_disk()?;
} }
let mut exercise_path = String::with_capacity(64);
let prefix = "exercises/";
exercise_path.push_str(prefix);
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
let dir = &self.exercise_dirs[exercise_files.dir_ind]; WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?;
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(".rs");
fs::write(&exercise_path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
} }
Ok(()) Ok(())
@ -100,8 +107,7 @@ impl EmbeddedFiles {
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
dir.init_on_disk()?; dir.init_on_disk()?;
fs::write(path, exercise_files.exercise) WriteStrategy::Overwrite.write(path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {path}"))
} }
/// Write the solution file to disk and return its path. /// Write the solution file to disk and return its path.
@ -110,25 +116,19 @@ impl EmbeddedFiles {
exercise_ind: usize, exercise_ind: usize,
exercise_name: &str, exercise_name: &str,
) -> Result<String> { ) -> Result<String> {
create_dir_if_not_exists("solutions")?;
let exercise_files = &self.exercise_files[exercise_ind]; let exercise_files = &self.exercise_files[exercise_ind];
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
// 14 = 10 + 1 + 3 // 14 = 10 + 1 + 3
// solutions/ + / + .rs // solutions/ + / + .rs
let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
dir_path.push_str("solutions/"); solution_path.push_str("solutions/");
dir_path.push_str(dir.name); solution_path.push_str(dir.name);
create_dir_if_not_exists(&dir_path)?;
let mut solution_path = dir_path;
solution_path.push('/'); solution_path.push('/');
solution_path.push_str(exercise_name); solution_path.push_str(exercise_name);
solution_path.push_str(".rs"); solution_path.push_str(".rs");
fs::write(&solution_path, exercise_files.solution) WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?;
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
Ok(solution_path) Ok(solution_path)
} }
@ -152,7 +152,7 @@ mod tests {
#[test] #[test]
fn dirs() { fn dirs() {
let exercises = toml::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file) let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
.expect("Failed to parse `info.toml`") .expect("Failed to parse `info.toml`")
.exercises; .exercises;

View File

@ -1,37 +1,15 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{ use ratatui::crossterm::style::{style, StyledContent, Stylize};
QueueableCommand, use std::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, fmt::{self, Display, Formatter},
io::Write,
}; };
use std::io::{self, StdoutLock, Write};
use crate::{ use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink};
cmd::CmdRunner,
term::{self, CountedWrite, file_path, terminal_file_link, write_ansi},
};
/// The initial capacity of the output buffer. /// The initial capacity of the output buffer.
pub const OUTPUT_CAPACITY: usize = 1 << 14; pub const OUTPUT_CAPACITY: usize = 1 << 14;
pub fn solution_link_line(
stdout: &mut StdoutLock,
solution_path: &str,
emit_file_links: bool,
) -> io::Result<()> {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"Solution")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" for comparison: ")?;
file_path(stdout, Color::Cyan, |writer| {
if emit_file_links && let Some(canonical_path) = term::canonicalize(solution_path) {
terminal_file_link(writer, solution_path, &canonical_path)
} else {
writer.stdout().write_all(solution_path.as_bytes())
}
})?;
stdout.write_all(b"\n")
}
// Run an exercise binary and append its output to the `output` buffer. // Run an exercise binary and append its output to the `output` buffer.
// Compilation must be done before calling this method. // Compilation must be done before calling this method.
fn run_bin( fn run_bin(
@ -40,25 +18,24 @@ fn run_bin(
cmd_runner: &CmdRunner, cmd_runner: &CmdRunner,
) -> Result<bool> { ) -> Result<bool> {
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
write_ansi(output, SetAttribute(Attribute::Underlined)); writeln!(output, "{}", "Output".underlined())?;
output.extend_from_slice(b"Output");
write_ansi(output, ResetColor);
output.push(b'\n');
} }
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
if let Some(output) = output if let Some(output) = output {
&& !success if !success {
{ // This output is important to show the user that something went wrong.
// This output is important to show the user that something went wrong. // Otherwise, calling something like `exit(1)` in an exercise without further output
// Otherwise, calling something like `exit(1)` in an exercise without further output // leaves the user confused about why the exercise isn't done yet.
// leaves the user confused about why the exercise isn't done yet. writeln!(
write_ansi(output, SetAttribute(Attribute::Bold)); output,
write_ansi(output, SetForegroundColor(Color::Red)); "{}",
output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); "The exercise didn't run successfully (nonzero exit code)"
write_ansi(output, ResetColor); .bold()
output.push(b'\n'); .red(),
)?;
}
} }
Ok(success) Ok(success)
@ -70,7 +47,6 @@ pub struct Exercise {
pub name: &'static str, pub name: &'static str,
/// Path of the exercise file starting with the `exercises/` directory. /// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str, pub path: &'static str,
pub canonical_path: Option<String>,
pub test: bool, pub test: bool,
pub strict_clippy: bool, pub strict_clippy: bool,
pub hint: &'static str, pub hint: &'static str,
@ -78,30 +54,25 @@ pub struct Exercise {
} }
impl Exercise { impl Exercise {
pub fn terminal_file_link<'a>( pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
&self, style(TerminalFileLink(self.path)).underlined().blue()
writer: &mut impl CountedWrite<'a>, }
emit_file_links: bool, }
) -> io::Result<()> {
file_path(writer, Color::Blue, |writer| { impl Display for Exercise {
if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
terminal_file_link(writer, self.path, canonical_path) self.path.fmt(f)
} else {
writer.write_str(self.path)
}
})
} }
} }
pub trait RunnableExercise { pub trait RunnableExercise {
fn name(&self) -> &str; fn name(&self) -> &str;
fn dir(&self) -> Option<&str>;
fn strict_clippy(&self) -> bool; fn strict_clippy(&self) -> bool;
fn test(&self) -> bool; fn test(&self) -> bool;
// Compile, check and run the exercise or its solution (depending on `bin_name´). // Compile, check and run the exercise or its solution (depending on `bin_name´).
// The output is written to the `output` buffer after clearing it. // The output is written to the `output` buffer after clearing it.
fn run<const FORCE_STRICT_CLIPPY: bool>( fn run(
&self, &self,
bin_name: &str, bin_name: &str,
mut output: Option<&mut Vec<u8>>, mut output: Option<&mut Vec<u8>>,
@ -127,7 +98,7 @@ pub trait RunnableExercise {
let output_is_some = output.is_some(); let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some { if output_is_some {
test_cmd.args(["--", "--color", "always", "--format", "pretty"]); test_cmd.args(["--", "--color", "always", "--show-output"]);
} }
let test_success = test_cmd.run("cargo test …")?; let test_success = test_cmd.run("cargo test …")?;
if !test_success { if !test_success {
@ -143,8 +114,8 @@ pub trait RunnableExercise {
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `#[cfg(test)]`. // `--profile test` is required to also check code with `[cfg(test)]`.
if FORCE_STRICT_CLIPPY || self.strict_clippy() { if self.strict_clippy() {
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]); clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
} else { } else {
clippy_cmd.args(["--profile", "test"]); clippy_cmd.args(["--profile", "test"]);
@ -160,7 +131,7 @@ pub trait RunnableExercise {
/// The output is written to the `output` buffer after clearing it. /// The output is written to the `output` buffer after clearing it.
#[inline] #[inline]
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> { fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
self.run::<false>(self.name(), output, cmd_runner) self.run(self.name(), output, cmd_runner)
} }
/// Compile, check and run the exercise's solution. /// Compile, check and run the exercise's solution.
@ -171,32 +142,7 @@ pub trait RunnableExercise {
bin_name.push_str(name); bin_name.push_str(name);
bin_name.push_str("_sol"); bin_name.push_str("_sol");
self.run::<true>(&bin_name, output, cmd_runner) self.run(&bin_name, output, cmd_runner)
}
fn sol_path(&self) -> String {
let name = self.name();
let mut path = if let Some(dir) = self.dir() {
// 14 = 10 + 1 + 3
// solutions/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + name.len());
path.push_str("solutions/");
path.push_str(dir);
path.push('/');
path
} else {
// 13 = 10 + 3
// solutions/ + .rs
let mut path = String::with_capacity(13 + name.len());
path.push_str("solutions/");
path
};
path.push_str(name);
path.push_str(".rs");
path
} }
} }
@ -206,11 +152,6 @@ impl RunnableExercise for Exercise {
self.name self.name
} }
#[inline]
fn dir(&self) -> Option<&str> {
self.dir
}
#[inline] #[inline]
fn strict_clippy(&self) -> bool { fn strict_clippy(&self) -> bool {
self.strict_clippy self.strict_clippy

View File

@ -1,4 +1,4 @@
use anyhow::{Context, Error, Result, bail}; use anyhow::{bail, Context, Error, Result};
use serde::Deserialize; use serde::Deserialize;
use std::{fs, io::ErrorKind}; use std::{fs, io::ErrorKind};
@ -52,6 +52,30 @@ impl ExerciseInfo {
path path
} }
/// Path to the solution file starting with the `solutions/` directory.
pub fn sol_path(&self) -> String {
let mut path = if let Some(dir) = &self.dir {
// 14 = 10 + 1 + 3
// solutions/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
path.push_str("solutions/");
path.push_str(dir);
path.push('/');
path
} else {
// 13 = 10 + 3
// solutions/ + .rs
let mut path = String::with_capacity(13 + self.name.len());
path.push_str("solutions/");
path
};
path.push_str(&self.name);
path.push_str(".rs");
path
}
} }
impl RunnableExercise for ExerciseInfo { impl RunnableExercise for ExerciseInfo {
@ -60,11 +84,6 @@ impl RunnableExercise for ExerciseInfo {
&self.name &self.name
} }
#[inline]
fn dir(&self) -> Option<&str> {
self.dir.as_deref()
}
#[inline] #[inline]
fn strict_clippy(&self) -> bool { fn strict_clippy(&self) -> bool {
self.strict_clippy self.strict_clippy
@ -79,7 +98,7 @@ impl RunnableExercise for ExerciseInfo {
/// The deserialized `info.toml` file. /// The deserialized `info.toml` file.
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct InfoFile { pub struct InfoFile {
/// For possible breaking changes in the future for community exercises. /// For possible breaking changes in the future for third-party exercises.
pub format_version: u8, pub format_version: u8,
/// Shown to users when starting with the exercises. /// Shown to users when starting with the exercises.
pub welcome_message: Option<String>, pub welcome_message: Option<String>,
@ -91,15 +110,15 @@ pub struct InfoFile {
impl InfoFile { impl InfoFile {
/// Official exercises: Parse the embedded `info.toml` file. /// Official exercises: Parse the embedded `info.toml` file.
/// Community exercises: Parse the `info.toml` file in the current directory. /// Third-party exercises: Parse the `info.toml` file in the current directory.
pub fn parse() -> Result<Self> { pub fn parse() -> Result<Self> {
// Read a local `info.toml` if it exists. // Read a local `info.toml` if it exists.
let slf = match fs::read_to_string("info.toml") { let slf = match fs::read_to_string("info.toml") {
Ok(file_content) => toml::de::from_str::<Self>(&file_content) Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content)
.context("Failed to parse the `info.toml` file")?, .context("Failed to parse the `info.toml` file")?,
Err(e) => { Err(e) => {
if e.kind() == ErrorKind::NotFound { if e.kind() == ErrorKind::NotFound {
return toml::de::from_str(EMBEDDED_FILES.info_file) return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
.context("Failed to parse the embedded `info.toml` file"); .context("Failed to parse the embedded `info.toml` file");
} }
@ -116,4 +135,4 @@ impl InfoFile {
} }
const NO_EXERCISES_ERR: &str = "There are no exercises yet! const NO_EXERCISES_ERR: &str = "There are no exercises yet!
Add at least one exercise before testing."; If you are developing third-party exercises, add at least one exercise before testing.";

View File

@ -1,8 +1,5 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use crossterm::{ use ratatui::crossterm::style::Stylize;
QueueableCommand,
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
env::set_current_dir, env::set_current_dir,
@ -13,8 +10,8 @@ use std::{
}; };
use crate::{ use crate::{
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, exercise::RunnableExercise, cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile,
info_file::InfoFile, term::press_enter_prompt, term::press_enter_prompt,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -35,27 +32,7 @@ pub fn init() -> Result<()> {
.stdin(Stdio::null()) .stdin(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.output() .output()
.context( .context(CARGO_LOCATE_PROJECT_ERR)?;
"Failed to run the command `cargo locate-project …`\n\
Did you already install Rust?\n\
Try running `cargo --version` to diagnose the problem.",
)?;
if !Command::new("cargo")
.arg("clippy")
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.context("Failed to run the command `cargo clippy --version`")?
.success()
{
bail!(
"Clippy, the official Rust linter, is missing.\n\
Please install it first before initializing Rustlings."
)
}
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
let mut init_git = true; let mut init_git = true;
@ -74,17 +51,13 @@ pub fn init() -> Result<()> {
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()))?; .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?;
if !workspace_manifest_content.contains("[workspace]") if !workspace_manifest_content.contains("[workspace]\n")
&& !workspace_manifest_content.contains("workspace.") && !workspace_manifest_content.contains("workspace.")
{ {
bail!( bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory");
"The current directory is already part of a Cargo project.\n\
Please initialize Rustlings in a different directory"
);
} }
stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\n\ stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?;
Press ENTER to continue ")?;
press_enter_prompt(&mut stdout)?; press_enter_prompt(&mut stdout)?;
// Make sure "rustlings" is added to `workspace.members` by making // Make sure "rustlings" is added to `workspace.members` by making
@ -99,10 +72,7 @@ pub fn init() -> Result<()> {
.stdout(Stdio::null()) .stdout(Stdio::null())
.status()?; .status()?;
if !status.success() { if !status.success() {
bail!( bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory");
"Failed to initialize a new Cargo workspace member.\n\
Please initialize Rustlings in a different directory"
);
} }
stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?; stdout.write_all(b"The directory `rustlings` has been added to `workspace.members` in the `Cargo.toml` file of this Cargo workspace.\n")?;
@ -110,8 +80,7 @@ pub fn init() -> Result<()> {
.context("Failed to remove the temporary directory `rustlings/`")?; .context("Failed to remove the temporary directory `rustlings/`")?;
init_git = false; init_git = false;
} else { } else {
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\n\ stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
Press ENTER to continue ")?;
press_enter_prompt(&mut stdout)?; press_enter_prompt(&mut stdout)?;
} }
@ -158,9 +127,6 @@ pub fn init() -> Result<()> {
fs::write("Cargo.toml", updated_cargo_toml) fs::write("Cargo.toml", updated_cargo_toml)
.context("Failed to create the file `rustlings/Cargo.toml`")?; .context("Failed to create the file `rustlings/Cargo.toml`")?;
fs::write("rust-analyzer.toml", RUST_ANALYZER_TOML)
.context("Failed to create the file `rustlings/rust-analyzer.toml`")?;
fs::write(".gitignore", GITIGNORE) fs::write(".gitignore", GITIGNORE)
.context("Failed to create the file `rustlings/.gitignore`")?; .context("Failed to create the file `rustlings/.gitignore`")?;
@ -173,34 +139,30 @@ pub fn init() -> Result<()> {
let _ = Command::new("git") let _ = Command::new("git")
.arg("init") .arg("init")
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status(); .status();
} }
stdout.queue(SetForegroundColor(Color::Green))?; writeln!(
stdout.write_all("Initialization done ✓".as_bytes())?; stdout,
stdout.queue(ResetColor)?; "\n{}\n\n{}",
stdout.write_all(b"\n\n")?; "Initialization done ✓".green(),
POST_INIT_MSG.bold(),
stdout.queue(SetAttribute(Attribute::Bold))?; )?;
stdout.write_all(POST_INIT_MSG)?;
stdout.queue(ResetColor)?;
Ok(()) Ok(())
} }
const CARGO_LOCATE_PROJECT_ERR: &str = "Failed to run the command `cargo locate-project …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
const INIT_SOLUTION_FILE: &[u8] = b"fn main() { const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
// DON'T EDIT THIS SOLUTION FILE! // DON'T EDIT THIS SOLUTION FILE!
// It will be automatically filled after you finish the exercise. // It will be automatically filled after you finish the exercise.
} }
"; ";
pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy"
check.extraArgs = ["--profile", "test"]
cargo.targetDir = true
"#;
const GITIGNORE: &[u8] = b"Cargo.lock const GITIGNORE: &[u8] = b"Cargo.lock
target/ target/
.vscode/ .vscode/
@ -219,6 +181,5 @@ You probably already initialized Rustlings.
Run `cd rustlings` Run `cd rustlings`
Then run `rustlings` again"; Then run `rustlings` again";
const POST_INIT_MSG: &[u8] = b"Run `cd rustlings` to go into the generated directory. const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started. Then run `rustlings` to get started.";
";

View File

@ -1,134 +1,93 @@
use anyhow::{Context, Result}; use anyhow::Result;
use crossterm::{ use ratatui::{
QueueableCommand, cursor, backend::CrosstermBackend,
event::{ crossterm::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind, event::{self, Event, KeyCode, KeyEventKind},
}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
terminal::{ ExecutableCommand,
DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode,
}, },
Terminal,
}; };
use std::io::{self, StdoutLock, Write}; use std::io;
use crate::app_state::AppState; use crate::app_state::AppState;
use self::state::{Filter, ListState}; use self::state::{Filter, UiState};
mod scroll_state;
mod state; mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::build(app_state, stdout)?;
let mut is_searching = false;
loop {
match event::read().context("Failed to read terminal event")? {
Event::Key(key) => {
match key.kind {
KeyEventKind::Release => continue,
KeyEventKind::Press | KeyEventKind::Repeat => (),
}
list_state.message.clear();
if is_searching {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
is_searching = false;
list_state.search_query.clear();
}
KeyCode::Char(c) => {
list_state.search_query.push(c);
list_state.apply_search_query();
}
KeyCode::Backspace => {
list_state.search_query.pop();
list_state.apply_search_query();
}
_ => continue,
}
list_state.draw(stdout)?;
continue;
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
KeyCode::Up | KeyCode::Char('k') => list_state.select_previous(),
KeyCode::Home | KeyCode::Char('g') => list_state.select_first(),
KeyCode::End | KeyCode::Char('G') => list_state.select_last(),
KeyCode::Char('d') => {
if list_state.filter() == Filter::Done {
list_state.set_filter(Filter::None);
list_state.message.push_str("Disabled filter DONE");
} else {
list_state.set_filter(Filter::Done);
list_state.message.push_str(
"Enabled filter DONE │ Press d again to disable the filter",
);
}
}
KeyCode::Char('p') => {
if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None);
list_state.message.push_str("Disabled filter PENDING");
} else {
list_state.set_filter(Filter::Pending);
list_state.message.push_str(
"Enabled filter PENDING │ Press p again to disable the filter",
);
}
}
KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
if list_state.selected_to_current_exercise()? {
return Ok(());
}
}
KeyCode::Char('s' | '/') => {
is_searching = true;
list_state.apply_search_query();
}
// Redraw to remove the message.
KeyCode::Esc => (),
_ => continue,
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => list_state.select_next(),
MouseEventKind::ScrollUp => list_state.select_previous(),
_ => continue,
},
Event::Resize(width, height) => list_state.set_term_size(width, height),
// Ignore
Event::FocusGained | Event::FocusLost => continue,
}
list_state.draw(stdout)?;
}
}
pub fn list(app_state: &mut AppState) -> Result<()> { pub fn list(app_state: &mut AppState) -> Result<()> {
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout stdout.execute(EnterAlternateScreen)?;
.queue(EnterAlternateScreen)?
.queue(cursor::Hide)?
.queue(DisableLineWrap)?
.queue(EnableMouseCapture)?;
enable_raw_mode()?; enable_raw_mode()?;
let res = handle_list(app_state, &mut stdout); let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
terminal.clear()?;
// Restore the terminal even if we got an error. let mut ui_state = UiState::new(app_state);
stdout
.queue(LeaveAlternateScreen)? 'outer: loop {
.queue(cursor::Show)? terminal.try_draw(|frame| ui_state.draw(frame).map_err(io::Error::other))?;
.queue(EnableLineWrap)?
.queue(DisableMouseCapture)? let key = loop {
.flush()?; match event::read()? {
Event::Key(key) => match key.kind {
KeyEventKind::Press | KeyEventKind::Repeat => break key,
KeyEventKind::Release => (),
},
// Redraw
Event::Resize(_, _) => continue 'outer,
// Ignore
Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
}
};
ui_state.message.clear();
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
KeyCode::Char('d') => {
let message = if ui_state.filter == Filter::Done {
ui_state.filter = Filter::None;
"Disabled filter DONE"
} else {
ui_state.filter = Filter::Done;
"Enabled filter DONE │ Press d again to disable the filter"
};
ui_state = ui_state.with_updated_rows();
ui_state.message.push_str(message);
}
KeyCode::Char('p') => {
let message = if ui_state.filter == Filter::Pending {
ui_state.filter = Filter::None;
"Disabled filter PENDING"
} else {
ui_state.filter = Filter::Pending;
"Enabled filter PENDING │ Press p again to disable the filter"
};
ui_state = ui_state.with_updated_rows();
ui_state.message.push_str(message);
}
KeyCode::Char('r') => {
ui_state = ui_state.with_reset_selected()?;
}
KeyCode::Char('c') => {
ui_state.selected_to_current_exercise()?;
ui_state = ui_state.with_updated_rows();
}
_ => (),
}
}
drop(terminal);
stdout.execute(LeaveAlternateScreen)?;
disable_raw_mode()?; disable_raw_mode()?;
res Ok(())
} }

View File

@ -1,104 +0,0 @@
pub struct ScrollState {
n_rows: usize,
max_n_rows_to_display: usize,
selected: Option<usize>,
offset: usize,
scroll_padding: usize,
max_scroll_padding: usize,
}
impl ScrollState {
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
Self {
n_rows,
max_n_rows_to_display: 0,
selected,
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
scroll_padding: 0,
max_scroll_padding,
}
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
fn update_offset(&mut self) {
let Some(selected) = self.selected else {
return;
};
let min_offset = (selected + self.scroll_padding)
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
let max_offset = selected.saturating_sub(self.scroll_padding);
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
self.offset = self
.offset
.max(min_offset)
.min(max_offset)
.min(global_max_offset);
}
#[inline]
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn set_selected(&mut self, selected: usize) {
self.selected = Some(selected);
self.update_offset();
}
pub fn select_next(&mut self) {
if let Some(selected) = self.selected {
self.set_selected((selected + 1).min(self.n_rows - 1));
}
}
pub fn select_previous(&mut self) {
if let Some(selected) = self.selected {
self.set_selected(selected.saturating_sub(1));
}
}
pub fn select_first(&mut self) {
if self.n_rows > 0 {
self.set_selected(0);
}
}
pub fn select_last(&mut self) {
if self.n_rows > 0 {
self.set_selected(self.n_rows - 1);
}
}
pub fn set_n_rows(&mut self, n_rows: usize) {
self.n_rows = n_rows;
if self.n_rows == 0 {
self.selected = None;
return;
}
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
}
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
self.max_n_rows_to_display = max_n_rows_to_display;
self.update_scroll_padding();
self.update_offset();
}
}

View File

@ -1,36 +1,14 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crossterm::{ use ratatui::{
QueueableCommand, layout::{Constraint, Rect},
cursor::{MoveTo, MoveToNextLine}, style::{Style, Stylize},
style::{ text::{Line, Span},
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
}, Frame,
terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate},
};
use std::{
fmt::Write as _,
io::{self, StdoutLock, Write},
}; };
use std::fmt::Write;
use crate::{ use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
app_state::AppState,
exercise::Exercise,
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)
.with(Attribute::Bold);
fn next_ln(stdout: &mut StdoutLock) -> io::Result<()> {
stdout
.queue(Clear(ClearType::UntilNewLine))?
.queue(MoveToNextLine(1))?;
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum Filter { pub enum Filter {
@ -39,379 +17,255 @@ pub enum Filter {
None, None,
} }
pub struct ListState<'a> { pub struct UiState<'a> {
/// Footer message to be displayed if not empty. pub table: Table<'static>,
pub message: String, pub message: String,
pub search_query: String, pub filter: Filter,
app_state: &'a mut AppState, app_state: &'a mut AppState,
scroll_state: ScrollState, table_state: TableState,
name_col_padding: Vec<u8>, n_rows: usize,
path_col_padding: Vec<u8>,
filter: Filter,
term_width: u16,
term_height: u16,
show_footer: bool,
} }
impl<'a> ListState<'a> { impl<'a> UiState<'a> {
pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> { pub fn with_updated_rows(mut self) -> Self {
stdout.queue(Clear(ClearType::All))?; let current_exercise_ind = self.app_state.current_exercise_ind();
let name_col_title_len = 4; self.n_rows = 0;
let path_col_title_len = 4; let rows = self
let (name_col_width, path_col_width) = app_state.exercises().iter().fold( .app_state
(name_col_title_len, path_col_title_len), .exercises()
|(name_col_width, path_col_width), exercise| { .iter()
( .enumerate()
name_col_width.max(exercise.name.len()), .filter_map(|(ind, exercise)| {
path_col_width.max(exercise.path.len()), let exercise_state = if exercise.done {
) if self.filter == Filter::Pending {
}, return None;
); }
let name_col_padding = vec![b' '; name_col_width + COL_SPACING];
let path_col_padding = vec![b' '; path_col_width]; "DONE".green()
} else {
if self.filter == Filter::Done {
return None;
}
"PENDING".yellow()
};
self.n_rows += 1;
let next = if ind == current_exercise_ind {
">>>>".bold().red()
} else {
Span::default()
};
Some(Row::new([
next,
exercise_state,
Span::raw(exercise.name),
Span::raw(exercise.path),
]))
});
self.table = self.table.rows(rows);
if self.n_rows == 0 {
self.table_state.select(None);
} else {
self.table_state.select(Some(
self.table_state
.selected()
.map_or(0, |selected| selected.min(self.n_rows - 1)),
));
}
self
}
pub fn new(app_state: &'a mut AppState) -> Self {
let header = Row::new(["Next", "State", "Name", "Path"]);
let max_name_len = app_state
.exercises()
.iter()
.map(|exercise| exercise.name.len())
.max()
.unwrap_or(4) as u16;
let widths = [
Constraint::Length(4),
Constraint::Length(7),
Constraint::Length(max_name_len),
Constraint::Fill(1),
];
let table = Table::default()
.widths(widths)
.header(header)
.column_spacing(2)
.highlight_spacing(HighlightSpacing::Always)
.highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
.highlight_symbol("🦀")
.block(Block::default().borders(Borders::BOTTOM));
let selected = app_state.current_exercise_ind();
let table_state = TableState::default()
.with_offset(selected.saturating_sub(10))
.with_selected(Some(selected));
let filter = Filter::None; let filter = Filter::None;
let n_rows_with_filter = app_state.exercises().len(); let n_rows = app_state.exercises().len();
let selected = app_state.current_exercise_ind();
let (width, height) = terminal::size().context("Failed to get the terminal size")?; let slf = Self {
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5); table,
let mut slf = Self {
message: String::with_capacity(128), message: String::with_capacity(128),
search_query: String::new(),
app_state,
scroll_state,
name_col_padding,
path_col_padding,
filter, filter,
// Set by `set_term_size` app_state,
term_width: 0, table_state,
term_height: 0, n_rows,
show_footer: true,
}; };
slf.set_term_size(width, height); slf.with_updated_rows()
slf.draw(stdout)?;
Ok(slf)
} }
pub fn set_term_size(&mut self, width: u16, height: u16) { pub fn select_next(&mut self) {
self.term_width = width; if self.n_rows > 0 {
self.term_height = height; let next = self
.table_state
if height == 0 { .selected()
return; .map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
self.table_state.select(Some(next));
} }
}
let header_height = 1; pub fn select_previous(&mut self) {
// 1 progress bar, 2 footer message lines. if self.n_rows > 0 {
let footer_height = 3; let previous = self
self.show_footer = height > header_height + footer_height; .table_state
.selected()
.map_or(0, |selected| selected.saturating_sub(1));
self.table_state.select(Some(previous));
}
}
self.scroll_state.set_max_n_rows_to_display( pub fn select_first(&mut self) {
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height) if self.n_rows > 0 {
as usize, self.table_state.select(Some(0));
}
}
pub fn select_last(&mut self) {
if self.n_rows > 0 {
self.table_state.select(Some(self.n_rows - 1));
}
}
pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
let area = frame.area();
frame.render_stateful_widget(
&self.table,
Rect {
x: 0,
y: 0,
width: area.width,
height: area.height - 3,
},
&mut self.table_state,
); );
}
fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> { frame.render_widget(
if !self.search_query.is_empty() Paragraph::new(progress_bar_ratatui(
&& let Some((pre_highlight, highlight, post_highlight)) = exercise
.name
.find(&self.search_query)
.and_then(|ind| exercise.name.split_at_checked(ind))
.and_then(|(pre_highlight, rest)| {
rest.split_at_checked(self.search_query.len())
.map(|x| (pre_highlight, x.0, x.1))
})
{
writer.write_str(pre_highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(highlight)?;
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
return writer.write_str(post_highlight);
}
writer.write_str(exercise.name)
}
fn draw_rows(
&self,
stdout: &mut StdoutLock,
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
) -> io::Result<usize> {
let current_exercise_ind = self.app_state.current_exercise_ind();
let row_offset = self.scroll_state.offset();
let mut n_displayed_rows = 0;
for (exercise_ind, exercise) in filtered_exercises
.skip(row_offset)
.take(self.scroll_state.max_n_rows_to_display())
{
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
// The crab emoji has the width of two ascii chars.
writer.add_to_len(2);
writer.stdout.write_all("🦀".as_bytes())?;
writer
.stdout
.queue(SetAttributes(SELECTED_ROW_ATTRIBUTES))?;
} else {
writer.write_ascii(b" ")?;
}
if exercise_ind == current_exercise_ind {
writer.stdout.queue(SetForegroundColor(Color::Red))?;
writer.write_ascii(b">>>>>>> ")?;
} else {
writer.write_ascii(b" ")?;
}
if exercise.done {
writer.stdout.queue(SetForegroundColor(Color::Green))?;
writer.write_ascii(b"DONE ")?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Yellow))?;
writer.write_ascii(b"PENDING")?;
}
writer.stdout.queue(SetForegroundColor(Color::Reset))?;
writer.write_ascii(b" ")?;
self.draw_exercise_name(&mut writer, exercise)?;
writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?;
exercise.terminal_file_link(&mut writer, self.app_state.emit_file_links())?;
writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?;
next_ln(stdout)?;
stdout.queue(ResetColor)?;
n_displayed_rows += 1;
}
Ok(n_displayed_rows)
}
pub fn draw(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_height == 0 {
return Ok(());
}
stdout.queue(BeginSynchronizedUpdate)?.queue(MoveTo(0, 0))?;
// Header
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b" Current State Name")?;
writer.write_ascii(&self.name_col_padding[4..])?;
writer.write_ascii(b"Path")?;
next_ln(stdout)?;
// Rows
let iter = self.app_state.exercises().iter().enumerate();
let n_displayed_rows = match self.filter {
Filter::Done => self.draw_rows(stdout, iter.filter(|(_, exercise)| exercise.done))?,
Filter::Pending => {
self.draw_rows(stdout, iter.filter(|(_, exercise)| !exercise.done))?
}
Filter::None => self.draw_rows(stdout, iter)?,
};
for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
next_ln(stdout)?;
}
if self.show_footer {
progress_bar(
&mut MaxLenWriter::new(stdout, self.term_width as usize),
self.app_state.n_done(), self.app_state.n_done(),
self.app_state.exercises().len() as u16, self.app_state.exercises().len() as u16,
self.term_width, area.width,
)?; )?)
next_ln(stdout)?; .block(Block::default().borders(Borders::BOTTOM)),
Rect {
x: 0,
y: area.height - 3,
width: area.width,
height: 2,
},
);
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); let message = if self.message.is_empty() {
if self.message.is_empty() { // Help footer.
// Help footer message let mut spans = Vec::with_capacity(4);
if self.scroll_state.selected().is_some() { spans.push(Span::raw(
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?; "↓/j ↑/k home/g end/G │ <c>ontinue at │ <r>eset │ filter ",
next_ln(stdout)?; ));
writer = MaxLenWriter::new(stdout, self.term_width as usize); match self.filter {
Filter::Done => {
writer.write_ascii(b"<s>earch | filter ")?; spans.push("<d>one".underlined().magenta());
} else { spans.push(Span::raw("/<p>ending"));
// Nothing selected (and nothing shown), so only display filter and quit.
writer.write_ascii(b"filter ")?;
} }
Filter::Pending => {
match self.filter { spans.push(Span::raw("<d>one/"));
Filter::Done => { spans.push("<p>ending".underlined().magenta());
writer
.stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.write_ascii(b"<d>one")?;
writer.stdout.queue(ResetColor)?;
writer.write_ascii(b"/<p>ending")?;
}
Filter::Pending => {
writer.write_ascii(b"<d>one/")?;
writer
.stdout
.queue(SetForegroundColor(Color::Magenta))?
.queue(SetAttribute(Attribute::Underlined))?;
writer.write_ascii(b"<p>ending")?;
writer.stdout.queue(ResetColor)?;
}
Filter::None => writer.write_ascii(b"<d>one/<p>ending")?,
} }
Filter::None => spans.push(Span::raw("<d>one/<p>ending")),
writer.write_ascii(b" | <q>uit list")?;
} else {
writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(&self.message)?;
stdout.queue(ResetColor)?;
next_ln(stdout)?;
} }
spans.push(Span::raw(" │ <q>uit"));
next_ln(stdout)?; Line::from(spans)
} } else {
Line::from(self.message.as_str().light_blue())
stdout.queue(EndSynchronizedUpdate)?.flush()
}
fn update_rows(&mut self) {
let n_rows = match self.filter {
Filter::Done => self
.app_state
.exercises()
.iter()
.filter(|exercise| exercise.done)
.count(),
Filter::Pending => self
.app_state
.exercises()
.iter()
.filter(|exercise| !exercise.done)
.count(),
Filter::None => self.app_state.exercises().len(),
}; };
frame.render_widget(
self.scroll_state.set_n_rows(n_rows); message,
} Rect {
x: 0,
#[inline] y: area.height - 1,
pub fn filter(&self) -> Filter { width: area.width,
self.filter height: 1,
} },
);
pub fn set_filter(&mut self, filter: Filter) {
self.filter = filter;
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();
}
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
match self.filter {
Filter::Done => self
.app_state
.exercises()
.iter()
.enumerate()
.filter(|(_, exercise)| exercise.done)
.nth(selected)
.context("Invalid selection index")
.map(|(ind, _)| ind),
Filter::Pending => self
.app_state
.exercises()
.iter()
.enumerate()
.filter(|(_, exercise)| !exercise.done)
.nth(selected)
.context("Invalid selection index")
.map(|(ind, _)| ind),
Filter::None => Ok(selected),
}
}
pub fn reset_selected(&mut self) -> Result<()> {
let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to reset!");
return Ok(());
};
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",
)?;
Ok(()) Ok(())
} }
pub fn apply_search_query(&mut self) { pub fn with_reset_selected(mut self) -> Result<Self> {
self.message.push_str("search:"); let Some(selected) = self.table_state.selected() else {
self.message.push_str(&self.search_query); return Ok(self);
self.message.push('|');
if self.search_query.is_empty() {
return;
}
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
let mut iter = self.app_state.exercises().iter();
let ind = match self.filter {
Filter::None => iter.position(is_search_result),
Filter::Done => iter
.filter(|exercise| exercise.done)
.position(is_search_result),
Filter::Pending => iter
.filter(|exercise| !exercise.done)
.position(is_search_result),
}; };
match ind { let ind = self
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind), .app_state
None => self.message.push_str(" (not found)"), .exercises()
} .iter()
.enumerate()
.filter_map(|(ind, exercise)| match self.filter {
Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind),
})
.nth(selected)
.context("Invalid selection index")?;
let exercise_path = self.app_state.reset_exercise_by_ind(ind)?;
write!(self.message, "The exercise {exercise_path} has been reset")?;
Ok(self.with_updated_rows())
} }
// Return `true` if there was something to select. pub fn selected_to_current_exercise(&mut self) -> Result<()> {
pub fn selected_to_current_exercise(&mut self) -> Result<bool> { let Some(selected) = self.table_state.selected() else {
let Some(selected) = self.scroll_state.selected() else { return Ok(());
self.message.push_str("Nothing selected to continue at!");
return Ok(false);
}; };
let exercise_ind = self.selected_to_exercise_ind(selected)?; let ind = self
self.app_state.set_current_exercise_ind(exercise_ind)?; .app_state
.exercises()
.iter()
.enumerate()
.filter_map(|(ind, exercise)| match self.filter {
Filter::Done => exercise.done.then_some(ind),
Filter::Pending => (!exercise.done).then_some(ind),
Filter::None => Some(ind),
})
.nth(selected)
.context("Invalid selection index")?;
Ok(true) self.app_state.set_current_exercise_ind(ind)
} }
} }

View File

@ -1,26 +1,29 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use app_state::StateFileStatus; use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::{ use std::{
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
process::ExitCode, process::exit,
}; };
use term::{clear_terminal, press_enter_prompt}; use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
mod app_state; mod app_state;
mod cargo_toml; mod cargo_toml;
mod cmd; mod cmd;
mod collections;
mod dev; mod dev;
mod embedded; mod embedded;
mod exercise; mod exercise;
mod info_file; mod info_file;
mod init; mod init;
mod list; mod list;
mod progress_bar;
mod run; mod run;
mod term; mod term;
mod terminal_link;
mod watch; mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1; const CURRENT_FORMAT_VERSION: u8 = 1;
@ -46,8 +49,6 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: Option<String>, name: Option<String>,
}, },
/// Check all the exercises, marking them as done or pending accordingly.
CheckAll,
/// Reset a single exercise /// Reset a single exercise
Reset { Reset {
/// The name of the exercise /// The name of the exercise
@ -58,31 +59,29 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: Option<String>, name: Option<String>,
}, },
/// Commands for developing (community) Rustlings exercises /// Commands for developing (third-party) Rustlings exercises
#[command(subcommand)] #[command(subcommand)]
Dev(DevCommands), Dev(DevCommands),
} }
fn main() -> Result<ExitCode> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() { if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
bail!("{OLD_METHOD_ERR}"); bail!("{OLD_METHOD_ERR}");
} }
'priority_cmd: { match args.command {
match args.command { Some(Subcommands::Init) => {
Some(Subcommands::Init) => init::init().context("Initialization failed")?, return init::init().context("Initialization failed");
Some(Subcommands::Dev(dev_command)) => dev_command.run()?,
_ => break 'priority_cmd,
} }
Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
return Ok(ExitCode::SUCCESS); _ => (),
} }
if !Path::new("exercises").is_dir() { if !Path::new("exercises").is_dir() {
println!("{PRE_INIT_MSG}"); println!("{PRE_INIT_MSG}");
return Ok(ExitCode::FAILURE); exit(1);
} }
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
@ -104,11 +103,7 @@ fn main() -> Result<ExitCode> {
clear_terminal(&mut stdout)?; clear_terminal(&mut stdout)?;
let welcome_message = welcome_message.trim_ascii(); let welcome_message = welcome_message.trim_ascii();
write!( write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
stdout,
"{welcome_message}\n\n\
Press ENTER to continue "
)?;
press_enter_prompt(&mut stdout)?; press_enter_prompt(&mut stdout)?;
clear_terminal(&mut stdout)?; clear_terminal(&mut stdout)?;
// Flush to be able to show errors occurring before printing a newline to stdout. // Flush to be able to show errors occurring before printing a newline to stdout.
@ -139,41 +134,21 @@ fn main() -> Result<ExitCode> {
) )
}; };
watch::watch(&mut app_state, notify_exercise_names)?; loop {
match watch::watch(&mut app_state, notify_exercise_names)? {
WatchExit::Shutdown => break,
// It is much easier to exit the watch mode, launch the list mode and then restart
// the watch mode instead of trying to pause the watch threads and correct the
// watch state.
WatchExit::List => list::list(&mut app_state)?,
}
}
} }
Some(Subcommands::Run { name }) => { Some(Subcommands::Run { name }) => {
if let Some(name) = name { if let Some(name) = name {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
} }
return run::run(&mut app_state); run::run(&mut app_state)?;
}
Some(Subcommands::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 {
app_state.set_current_exercise_ind(first_pending_exercise_ind)?;
}
stdout.write_all(b"\n\n")?;
let pending = app_state.n_pending();
if pending == 1 {
stdout.write_all(b"One exercise pending: ")?;
} else {
write!(
stdout,
"{pending}/{} exercises pending. The first: ",
app_state.exercises().len(),
)?;
}
app_state
.current_exercise()
.terminal_file_link(&mut stdout, app_state.emit_file_links())?;
stdout.write_all(b"\n")?;
return Ok(ExitCode::FAILURE);
} else {
app_state.render_final_message(&mut stdout)?;
}
} }
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
@ -190,7 +165,7 @@ fn main() -> Result<ExitCode> {
Some(Subcommands::Init | Subcommands::Dev(_)) => (), Some(Subcommands::Init | Subcommands::Dev(_)) => (),
} }
Ok(ExitCode::SUCCESS) Ok(())
} }
const OLD_METHOD_ERR: &str = const OLD_METHOD_ERR: &str =

100
src/progress_bar.rs Normal file
View File

@ -0,0 +1,100 @@
use anyhow::{bail, Result};
use ratatui::text::{Line, Span};
use std::fmt::Write;
const PREFIX: &str = "Progress: [";
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
// Leaving the last char empty (_) for `total` > 99.
const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
const PROGRESS_EXCEEDS_MAX_ERR: &str =
"The progress of the progress bar is higher than the maximum";
/// Terminal progress bar to be used when not using Ratataui.
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
use ratatui::crossterm::style::Stylize;
if progress > total {
bail!(PROGRESS_EXCEEDS_MAX_ERR);
}
if line_width < MIN_LINE_WIDTH {
return Ok(format!("Progress: {progress}/{total} exercises"));
}
let mut line = String::with_capacity(usize::from(line_width));
line.push_str(PREFIX);
let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
let mut green_part = String::with_capacity(usize::from(filled + 1));
for _ in 0..filled {
green_part.push('#');
}
if filled < width {
green_part.push('>');
}
write!(line, "{}", green_part.green()).unwrap();
let width_minus_filled = width - filled;
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
let mut red_part = String::with_capacity(usize::from(red_part_width));
for _ in 0..red_part_width {
red_part.push('-');
}
write!(line, "{}", red_part.red()).unwrap();
}
writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
Ok(line)
}
/// Progress bar to be used with Ratataui.
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
use ratatui::style::Stylize;
if progress > total {
bail!(PROGRESS_EXCEEDS_MAX_ERR);
}
if line_width < MIN_LINE_WIDTH {
return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
}
let mut spans = Vec::with_capacity(4);
spans.push(Span::raw(PREFIX));
let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total;
let mut green_part = String::with_capacity(usize::from(filled + 1));
for _ in 0..filled {
green_part.push('#');
}
if filled < width {
green_part.push('>');
}
spans.push(green_part.green());
let width_minus_filled = width - filled;
if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1;
let mut red_part = String::with_capacity(usize::from(red_part_width));
for _ in 0..red_part_width {
red_part.push('-');
}
spans.push(red_part.red());
}
spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
Ok(Line::from(spans))
}

View File

@ -1,19 +1,14 @@
use anyhow::Result; use anyhow::{bail, Result};
use crossterm::{ use ratatui::crossterm::style::{style, Stylize};
QueueableCommand, use std::io::{self, Write};
style::{Color, ResetColor, SetForegroundColor},
};
use std::{
io::{self, Write},
process::ExitCode,
};
use crate::{ use crate::{
app_state::{AppState, ExercisesProgress}, app_state::{AppState, ExercisesProgress},
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, exercise::{RunnableExercise, OUTPUT_CAPACITY},
terminal_link::TerminalFileLink,
}; };
pub fn run(app_state: &mut AppState) -> Result<ExitCode> { pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?; let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
@ -24,37 +19,33 @@ pub fn run(app_state: &mut AppState) -> Result<ExitCode> {
if !success { if !success {
app_state.set_pending(app_state.current_exercise_ind())?; app_state.set_pending(app_state.current_exercise_ind())?;
stdout.write_all(b"Ran ")?; bail!(
app_state "Ran {} with errors",
.current_exercise() app_state.current_exercise().terminal_link(),
.terminal_file_link(&mut stdout, app_state.emit_file_links())?; );
stdout.write_all(b" with errors\n")?;
return Ok(ExitCode::FAILURE);
} }
stdout.queue(SetForegroundColor(Color::Green))?; writeln!(
stdout.write_all("✓ Successfully ran ".as_bytes())?; stdout,
stdout.write_all(exercise.path.as_bytes())?; "{}{}",
stdout.queue(ResetColor)?; "✓ Successfully ran ".green(),
stdout.write_all(b"\n")?; exercise.path.green(),
)?;
if let Some(solution_path) = app_state.current_solution_path()? { if let Some(solution_path) = app_state.current_solution_path()? {
stdout.write_all(b"\n")?; println!(
solution_link_line(&mut stdout, &solution_path, app_state.emit_file_links())?; "\nA solution file can be found at {}\n",
stdout.write_all(b"\n")?; style(TerminalFileLink(&solution_path)).underlined().green(),
);
} }
match app_state.done_current_exercise::<false>(&mut stdout)? { match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
stdout.write_all(b"Next exercise: ")?;
app_state
.current_exercise()
.terminal_file_link(&mut stdout, app_state.emit_file_links())?;
stdout.write_all(b"\n")?;
}
ExercisesProgress::AllDone => (), ExercisesProgress::AllDone => (),
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
"Next exercise: {}",
app_state.current_exercise().terminal_link(),
),
} }
Ok(ExitCode::SUCCESS) Ok(())
} }

Some files were not shown because too many files have changed in this diff Show More