diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..b8fa3f77 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +dev = ["run", "--", "dev"] diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index aab09aa3..00000000 --- a/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -root = true - -[*.rs] -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 80f052d6..a52d0730 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,10 +1,18 @@ -name: Rustlings Tests +name: Check on: push: branches: [main] + paths-ignore: + - website + - .github/workflows/website.yml + - '*.md' pull_request: branches: [main] + paths-ignore: + - website + - .github/workflows/website.yml + - '*.md' env: CARGO_TERM_COLOR: always @@ -13,31 +21,36 @@ jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: cargo clippy -- --deny warnings + - uses: actions/checkout@v6 + - name: Clippy + run: cargo clippy -- --deny warnings fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@v16 - with: - globs: "exercises/**/*.md" - - name: Run cargo fmt + - uses: actions/checkout@v6 + - name: rustfmt run: cargo fmt --all --check test: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - - name: Run cargo test + - name: cargo test + env: + RUST_BACKTRACE: 1 run: cargo test --workspace dev-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: swatinem/rust-cache@v2 - - name: Run rustlings dev check - run: cargo run -- dev check --require-solutions + - name: rustlings dev check + run: cargo dev check --require-solutions + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml deleted file mode 100644 index ec5d4462..00000000 --- a/.github/workflows/web.yml +++ /dev/null @@ -1,87 +0,0 @@ -# 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 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 00000000..135cd1e2 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,50 @@ +name: Website + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - website + - .github/workflows/website.yml + +jobs: + rumdl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rvben/rumdl@v0 + build: + needs: rumdl + defaults: + run: + working-directory: website + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - 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.22.1/zola-v0.22.1-x86_64-unknown-linux-gnu.tar.gz | tar xz + - name: Build site + run: ./zola build + - name: Upload static files as artifact + uses: actions/upload-pages-artifact@v5 + 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@v5 diff --git a/.gitignore b/.gitignore index 945382c3..ea65eb1e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,6 @@ Cargo.lock # State file .rustlings-state.txt -# oranda -public/ -.netlify - # OS .DS_Store .direnv/ diff --git a/.markdownlint.yml b/.markdownlint.yml deleted file mode 100644 index d5f7e391..00000000 --- a/.markdownlint.yml +++ /dev/null @@ -1,2 +0,0 @@ -# MD013/line-length Line length, Expected: 80 -MD013: false diff --git a/.rumdl.toml b/.rumdl.toml new file mode 100644 index 00000000..528e0e31 --- /dev/null +++ b/.rumdl.toml @@ -0,0 +1,7 @@ +[global] +output-format = "full" +disable = ["MD013", "MD057"] + +[per-file-ignores] +"website/content/_index.md" = ["MD041"] +"website/content/**/*.md" = ["MD028", "MD033"] diff --git a/CHANGELOG.md b/CHANGELOG.md index a2085b91..807f7d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,47 @@ - +# Changelog + +## Unreleased + +### Added + +- Automatically open the current file if Rustlings is running in a VS Code terminal +- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) +- New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij +- New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise +- Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` + +### Fixed + +- Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec) +- Fix workspace detection on Windows [@senekor](https://github.com/senekor) + +### Changed + +- Avoid initializing a nested Git repository [@senekor](https://github.com/senekor) +- `vecs2`: Removed the use of `map` and `collect`, which are only taught later. +- `structs3`: Rewrote the exercise to make users type method syntax themselves. +- Rename the exercises for smart pointers and conversions so they're sorted alphabetically. [@foxfromworld](https://github.com/foxfromworld) +- `vecs1`: Remove array literal. Some learners assumed their task is to convert it to a vector. +- `conversions2`: Redesign the context such that infallible conversion makes sense. + +## 6.5.0 (2025-08-21) + +### 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) @@ -11,7 +54,7 @@ - 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 third-party exercises willing to reach that limit? πŸ” +- Limit the maximum number of exercises to 999. Any community exercises willing to reach that limit? πŸ” ### Changed @@ -29,8 +72,6 @@ - Fix bad contrast in the list on terminals with a light theme. - - ## 6.3.0 (2024-08-29) ### Added @@ -70,8 +111,6 @@ - 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) ### Added @@ -88,34 +127,28 @@ - Run the final check of all exercises in parallel. - Small exercise improvements. - - ## 6.1.0 (2024-07-10) -#### Added +### Added -- `dev check`: Check that all exercises (including third-party ones) include at least one `TODO` comment. +- `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment. - `dev check`: Check that all exercises actually fail to run (not already solved). -#### Changed +### Changed - Make enum variants more consistent between enum exercises. - `iterators3`: Teach about the possible case of integer overflow during division. -#### Fixed +### Fixed - Exit with a helpful error message on missing/unsupported terminal/TTY. - Mark the last exercise as done. - - ## 6.0.1 (2024-07-04) Small exercise improvements and fixes. Most importantly, fixed that the exercise `clippy1` was already solved πŸ˜… - - ## 6.0.0 (2024-07-03) This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨ @@ -173,23 +206,21 @@ 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 πŸ“Ž Make Clippy your friend from early on πŸ₯° -### Third-party exercises +### Community Exercises -Rustlings now supports third-party exercises! +Rustlings now supports community 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](THIRD_PARTY_EXERCISES.md)! - - +Then follow the link to the guide about [community exercises](https://rustlings.rust-lang.org/community-exercises)! ## 5.6.1 (2023-09-18) -#### Changed +### Changed - Converted all exercises with assertions to test mode. -#### Fixed +### Fixed - `cow1`: Reverted regression introduced by calling `to_mut` where it shouldn't have been called, and clarified comment. @@ -198,11 +229,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `as_ref_mut`: Fixed a typo in a test function name. - `enums3`: Fixed formatting with `rustfmt`. - - ## 5.6.0 (2023-09-04) -#### Added +### Added - New exercise: `if3`, teaching the user about `if let` statements. - `hashmaps2`: Added an extra test function to check if the amount of fruits is higher than zero. @@ -210,7 +239,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `if1`: Added a test case to check equal values. - `if3`: Added a note specifying that there are no test changes needed. -#### Changed +### Changed - Swapped the order of threads and smart pointer exercises. - Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :) @@ -218,7 +247,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same concepts. -#### Fixed +### Fixed - `iterators5`: - Removed an outdated part of the hint. @@ -233,25 +262,21 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `cow1`: Added `.to_mut()` to distinguish from the previous test case. - `threads2`: Updated hint text to reference the correct book heading. -#### Housekeeping +### Housekeeping - Cleaned up the explanation paragraphs at the start of each exercise. - Lots of Nix housekeeping that I don't feel qualified to write about! - Improved CI workflows, we're now testing on multiple platforms at once. - - ## 5.5.1 (2023-05-17) -#### Fixed +### Fixed - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. - - ## 5.5.0 (2023-05-17) -#### Added +### Added - `strings2`: Added a reference to the book chapter for reference conversion - `lifetimes`: Added a link to the lifetimekata project @@ -259,7 +284,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Added a `!` prefix command to watch mode that runs an external command - Added a `--success-hints` option to watch mode that shows hints on exercise success -#### Changed +### Changed - `vecs2`: Renamed iterator variable bindings for clarify - `lifetimes`: Changed order of book references @@ -268,7 +293,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `options2`: Improved tests for layering options - `modules2`: Added more information to the hint -#### Fixed +### Fixed - `errors2`: Corrected a comment wording - `iterators2`: Fixed a spelling mistake in the hint text @@ -278,33 +303,29 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `options3`: Changed exercise to panic when no match is found - `rustlings lsp` now generates absolute paths, which should fix VSCode `rust-analyzer` usage on Windows -#### Housekeeping +### Housekeeping - Added a markdown linter to run on GitHub actions - Split quick installation section into two code blocks - - ## 5.4.1 (2023-03-10) -#### Changed +### Changed - `vecs`: Added links to `iter_mut` and `map` to README.md - `cow1`: Changed main to tests - `iterators1`: Formatted according to rustfmt -#### Fixed +### Fixed - `errors5`: Unified undisclosed type notation - `arc1`: Improved readability by avoiding implicit dereference - `macros4`: Prevented auto-fix by adding `#[rustfmt::skip]` - `cli`: Actually show correct progress percentages - - ## 5.4.0 (2023-02-12) -#### Changed +### Changed - Reordered exercises - Unwrapped `standard_library_types` into `iterators` and `smart_pointers` @@ -316,7 +337,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Made progress bar update proportional to amount of files verified - Decreased `watch` delay from 2 to 1 second -#### Fixed +### Fixed - Capitalized "Rust" in exercise hints - **enums3**: Removed superfluous tuple brackets @@ -326,25 +347,23 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Fixed a typo in a method name - Specified the edition in `rustc` commands -#### Housekeeping +### Housekeeping - Bumped min Rust version to 1.58 in installation script - - ## 5.3.0 (2022-12-23) -#### Added +### Added - **cli**: Added a percentage display in watch mode - Added a `flake.nix` for Nix users -#### Changed +### Changed - **structs3**: Added an additional test - **macros**: Added a link to MacroKata in the README -#### Fixed +### Fixed - **strings3**: Added a link to `std` in the hint - **threads1**: Corrected a hint link @@ -358,63 +377,55 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **enums2**: Removed unnecessary indirection of self - **enums3**: Added an extra tuple comment -#### Housekeeping +### Housekeeping - Added a VSCode extension recommendation - Applied some Clippy and rustfmt formatting - Added a note on Windows PowerShell and other shell compatibility - - ## 5.2.1 (2022-09-06) -#### Fixed +### Fixed - **quiz1**: Reworded the comment to actually reflect what's going on in the tests. Also added another assert just to make sure. - **rc1**: Fixed a typo in the hint. - **lifetimes**: Add quotes to the `println!` output, for readability. -#### Housekeeping +### Housekeeping - Fixed a typo in README.md - - ## 5.2.0 (2022-08-27) -#### Added +### Added - Added a `reset` command -#### Changed +### Changed - **options2**: Convert the exercise to use tests -#### Fixed +### Fixed - **threads3**: Fixed a typo - **quiz1**: Adjusted the explanations to be consistent with the tests - - ## 5.1.1 (2022-08-17) -#### Bug Fixes +### Bug Fixes - Fixed an incorrect assertion in options1 - - ## 5.1.0 (2022-08-16) -#### Features +### Features - Added a new `rc1` exercise. - Added a new `cow1` exercise. -#### Bug Fixes +### Bug Fixes - **variables5**: Corrected reference to previous exercise - **functions4**: Fixed line number reference @@ -434,18 +445,16 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Added more granular tests - Fixed some comment syntax shenanigans in info.toml -#### Housekeeping +### Housekeeping - Fixed a typo in .editorconfig - Fixed a typo in integration_tests.rs - Clarified manual installation instructions using `cargo install --path .` - Added a link to our Zulip in the readme file - - ## 5.0.0 (2022-07-16) -#### Features +### Features - Hint comments in exercises now also include a reference to the `hint` watch mode subcommand. @@ -477,7 +486,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Added 3 new lifetimes exercises. - Added 3 new traits exercises. -#### Bug Fixes +### Bug Fixes - **variables2**: Made output messages more verbose. - **variables5**: Added a nudging hint about shadowing. @@ -501,7 +510,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER `Box`. - **try_from_into**: Fixed the function name in comment. -#### Removed +### Removed - Removed the legacy LSP feature that was using `mod.rs` files. - Removed `quiz4`. @@ -509,67 +518,61 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER order, and I've always felt like they didn't quite fit in with the mostly simple, book-following style we've had in Rustlings. -#### Housekeeping +### Housekeeping - Added missing exercises to the book index. - Updated spacing in Cargo.toml. - Added a GitHub actions config so that tests run on every PR/commit. - - ## 4.8.0 (2022-07-01) -#### Features +### Features - Added a progress indicator for `rustlings watch`. - The installation script now checks for Rustup being installed. - Added a `rustlings lsp` command to enable `rust-analyzer`. -#### Bug Fixes +### Bug Fixes - **move_semantics5**: Replaced "in vogue" with "in scope" in hint. - **if2**: Fixed a typo in the hint. - **variables1**: Fixed an incorrect line reference in the hint. - Fixed an out of bounds check in the installation Bash script. -#### Housekeeping +### Housekeeping - Replaced the git.io URL with the fully qualified URL because of git.io's sunsetting. - Removed the deprecated Rust GitPod extension. - - ## 4.7.1 (2022-04-20) -#### Features +### Features - The amount of dependency crates that need to be compiled went down from ~65 to ~45 by bumping dependency versions. - The minimum Rust version in the install scripts has been bumped to 1.56.0 (this isn't in the release itself, since install scripts don't really get versioned) -#### Bug Fixes +### Bug Fixes - **arc1**: A small part has been rewritten using a more functional code style (#968). - **using_as**: A small part has been refactored to use `sum` instead of `fold`, resulting in better readability. -#### Housekeeping +### Housekeeping - The changelog will now be manually written instead of being automatically generated by the Git log. - - ## 4.7.0 (2022-04-14) -#### Features +### Features - Add move_semantics6.rs exercise (#908) ([3f0e1303](https://github.com/rust-lang/rustlings/commit/3f0e1303e0b3bf3fecc0baced3c8b8a37f83c184)) - **intro:** Add intro section. ([21c9f441](https://github.com/rust-lang/rustlings/commit/21c9f44168394e08338fd470b5f49b1fd235986f)) - Include exercises folder in the project structure behind a feature, enabling rust-analyzer to work (#917) ([179a75a6](https://github.com/rust-lang/rustlings/commit/179a75a68d03ac9518dec2297fb17f91a4fc506b)) -#### Bug Fixes +### Bug Fixes - Fix a few spelling mistakes ([1c0fe3cb](https://github.com/rust-lang/rustlings/commit/1c0fe3cbcca85f90b3985985b8e265ee872a2ab2)) - **cli:** @@ -596,16 +599,14 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **structs3.rs:** assigned value to cents_per_gram in test ([d1ee2daf](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) - **traits1:** rename test functions to snake case (#854) ([1663a16e](https://github.com/rust-lang/rustlings/commit/1663a16eade6ca646b6ed061735f7982434d530d)) -#### Documentation improvements +### Documentation improvements - Add hints on how to get GCC installed (#741) ([bc56861](https://github.com/rust-lang/rustlings/commit/bc5686174463ad6f4f6b824b0e9b97c3039d4886)) - Fix some code blocks that were not highlighted ([17f9d74](https://github.com/rust-lang/rustlings/commit/17f9d7429ccd133a72e815fb5618e0ce79560929)) - - ## 4.6.0 (2021-09-25) -#### Features +### Features - add advanced_errs2 ([abd6b70c](https://github.com/rust-lang/rustlings/commit/abd6b70c72dc6426752ff41f09160b839e5c449e)) - add advanced_errs1 ([882d535b](https://github.com/rust-lang/rustlings/commit/882d535ba8628d5e0b37e8664b3e2f26260b2671)) @@ -614,7 +615,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **modules:** update exercises, add modules3 (#822) ([dfd2fab4](https://github.com/rust-lang/rustlings/commit/dfd2fab4f33d1bf59e2e5ee03123c0c9a67a9481)) - **quiz1:** add default function name in comment (#838) ([0a11bad7](https://github.com/rust-lang/rustlings/commit/0a11bad71402b5403143d642f439f57931278c07)) -#### Bug Fixes +### Bug Fixes - Correct small typo in exercises/conversions/from_str.rs ([86cc8529](https://github.com/rust-lang/rustlings/commit/86cc85295ae36948963ae52882e285d7e3e29323)) - **cli:** typo in exercise.rs (#848) ([06d5c097](https://github.com/rust-lang/rustlings/commit/06d5c0973a3dffa3c6c6f70acb775d4c6630323c)) @@ -625,16 +626,14 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - 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)) - - ## 4.5.0 (2021-07-07) -#### Features +### Features - Add move_semantics5 exercise. (#746) ([399ab328](https://github.com/rust-lang/rustlings/commit/399ab328d8d407265c09563aa4ef4534b2503ff2)) - **cli:** Add "next" to run the next unsolved exercise. (#785) ([d20e413a](https://github.com/rust-lang/rustlings/commit/d20e413a68772cd493561f2651cf244e822b7ca5)) -#### Bug Fixes +### Bug Fixes - rename result1 to errors4 ([50ab289d](https://github.com/rust-lang/rustlings/commit/50ab289da6b9eb19a7486c341b00048c516b88c0)) - move_semantics5 hints ([1b858285](https://github.com/rust-lang/rustlings/commit/1b85828548f46f58b622b5e0c00f8c989f928807)) @@ -647,11 +646,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **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)) - - ## 4.4.0 (2021-04-24) -#### Bug Fixes +### Bug Fixes - Fix spelling error in main.rs ([91ee27f2](https://github.com/rust-lang/rustlings/commit/91ee27f22bd3797a9db57e5fd430801c170c5db8)) - typo in default out text ([644c49f1](https://github.com/rust-lang/rustlings/commit/644c49f1e04cbb24e95872b3a52b07d692ae3bc8)) @@ -679,7 +676,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **threads1:** line number correction ([7857b0a6](https://github.com/rust-lang/rustlings/commit/7857b0a689b0847f48d8c14cbd1865e3b812d5ca)) - **try_from_into:** use trait objects ([2e93a588](https://github.com/rust-lang/rustlings/commit/2e93a588e0abe8badb7eafafb9e7d073c2be5df8)) -#### Features +### Features - Replace clap with argh ([7928122f](https://github.com/rust-lang/rustlings/commit/7928122fcef9ca7834d988b1ec8ca0687478beeb)) - Replace emojis when NO_EMOJI env variable present ([8d62a996](https://github.com/rust-lang/rustlings/commit/8d62a9963708dbecd9312e8bcc4b47049c72d155)) @@ -690,11 +687,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913)) - added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4)) - - ## 4.3.0 (2020-12-29) -#### Features +### Features - Rewrite default out text ([44d39112](https://github.com/rust-lang/rustlings/commit/44d39112ff122b29c9793fe52e605df1612c6490)) - match exercise order to book chapters (#541) ([033bf119](https://github.com/rust-lang/rustlings/commit/033bf1198fc8bfce1b570e49da7cde010aa552e3)) @@ -702,7 +697,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - add "rustlings list" command ([838f9f30](https://github.com/rust-lang/rustlings/commit/838f9f30083d0b23fd67503dcf0fbeca498e6647)) - **try_from_into:** remove duplicate annotation ([04f1d079](https://github.com/rust-lang/rustlings/commit/04f1d079aa42a2f49af694bc92c67d731d31a53f)) -#### Bug Fixes +### Bug Fixes - update structs README ([bcf14cf6](https://github.com/rust-lang/rustlings/commit/bcf14cf677adb3a38a3ac3ca53f3c69f61153025)) - added missing exercises to info.toml ([90cfb6ff](https://github.com/rust-lang/rustlings/commit/90cfb6ff28377531bfc34acb70547bdb13374f6b)) @@ -714,18 +709,16 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - 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)) - - ## 4.2.0 (2020-11-07) -#### Features +### Features - Add HashMap exercises ([633c00cf](https://github.com/rust-lang/rustlings/commit/633c00cf8071e1e82959a3010452a32f34f29fc9)) - Add Vec exercises ([0c12fa31](https://github.com/rust-lang/rustlings/commit/0c12fa31c57c03c6287458a0a8aca7afd057baf6)) - **primitive_types6:** Add a test (#548) ([2b1fb2b7](https://github.com/rust-lang/rustlings/commit/2b1fb2b739bf9ad8d6b7b12af25fee173011bfc4)) - **try_from_into:** Add tests (#571) ([95ccd926](https://github.com/rust-lang/rustlings/commit/95ccd92616ae79ba287cce221101e0bbe4f68cdc)) -#### Bug Fixes +### Bug Fixes - log error output when inotify limit is exceeded ([d61b4e5a](https://github.com/rust-lang/rustlings/commit/d61b4e5a13b44d72d004082f523fa1b6b24c1aca)) - more unique temp_file ([5643ef05](https://github.com/rust-lang/rustlings/commit/5643ef05bc81e4a840e9456f4406a769abbe1392)) @@ -736,11 +729,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - 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)) - - ## 4.1.0 (2020-10-05) -#### Bug Fixes +### Bug Fixes - Update rustlings version in Cargo.lock ([1cc40bc9](https://github.com/rust-lang/rustlings/commit/1cc40bc9ce95c23d56f6d91fa1c4deb646231fef)) - **arc1:** index mod should equal thread count ([b4062ef6](https://github.com/rust-lang/rustlings/commit/b4062ef6993e80dac107c4093ea85166ad3ee0fa)) @@ -750,7 +741,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **structs3:** Small adjustment of variable name ([114b54cb](https://github.com/rust-lang/rustlings/commit/114b54cbdb977234b39e5f180d937c14c78bb8b2)) - **using_as:** Add test so that proper type is returned. (#512) ([3286c5ec](https://github.com/rust-lang/rustlings/commit/3286c5ec19ea5fb7ded81d047da5f8594108a490)) -#### Features +### Features - Added iterators1.rs exercise ([9642f5a3](https://github.com/rust-lang/rustlings/commit/9642f5a3f686270a4f8f6ba969919ddbbc4f7fdd)) - Add ability to run rustlings on repl.it (#471) ([8f7b5bd0](https://github.com/rust-lang/rustlings/commit/8f7b5bd00eb83542b959830ef55192d2d76db90a)) @@ -760,16 +751,14 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **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)) - - ## 4.0.0 (2020-07-08) -#### Breaking Changes +### Breaking Changes - Add a --nocapture option to display test harnesses' outputs ([8ad5f9bf](https://github.com/rust-lang/rustlings/commit/8ad5f9bf531a4848b1104b7b389a20171624c82f)) - Rename test to quiz, fixes #244 ([010a0456](https://github.com/rust-lang/rustlings/commit/010a04569282149cea7f7a76fc4d7f4c9f0f08dd)) -#### Features +### Features - Add traits README ([173bb141](https://github.com/rust-lang/rustlings/commit/173bb14140c5530cbdb59e53ace3991a99d804af)) - Add box1.rs exercise ([7479a473](https://github.com/rust-lang/rustlings/commit/7479a4737bdcac347322ad0883ca528c8675e720)) @@ -778,7 +767,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Added exercise structs3.rs ([b66e2e09](https://github.com/rust-lang/rustlings/commit/b66e2e09622243e086a0f1258dd27e1a2d61c891)) - Add exercise variables6 covering const (#352) ([5999acd2](https://github.com/rust-lang/rustlings/commit/5999acd24a4f203292be36e0fd18d385887ec481)) -#### Bug Fixes +### Bug Fixes - Change then to than ([ddd98ad7](https://github.com/rust-lang/rustlings/commit/ddd98ad75d3668fbb10eff74374148aa5ed2344d)) - rename quiz1 to tests1 in info (#420) ([0dd1c6ca](https://github.com/rust-lang/rustlings/commit/0dd1c6ca6b389789e0972aa955fe17aa15c95f29)) @@ -803,15 +792,13 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **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)) - - ## 3.0.0 (2020-04-11) -#### Breaking Changes +### Breaking Changes - make "compile" exercises print output (#278) ([3b6d5c](https://github.com/fmoko/rustlings/commit/3b6d5c3aaa27a242a832799eb66e96897d26fde3)) -#### Bug Fixes +### Bug Fixes - **primitive_types:** revert primitive_types4 (#296) ([b3a3351e](https://github.com/rust-lang/rustlings/commit/b3a3351e8e6a0bdee07077d7b0382953821649ae)) - **run:** compile clippy exercise files (#295) ([3ab084a4](https://github.com/rust-lang/rustlings/commit/3ab084a421c0f140ae83bf1fc3f47b39342e7373)) @@ -820,30 +807,26 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342)) - don't hardcode documentation version for traits (#288) ([30e6af](https://github.com/fmoko/rustlings/commit/30e6af60690c326fb5d3a9b7335f35c69c09137d)) -#### Features +### Features - add Option2 exercise (#290) ([86b5c08b](https://github.com/rust-lang/rustlings/commit/86b5c08b9bea1576127a7c5f599f5752072c087d)) - add exercise for option (#282) ([135e5d47](https://github.com/rust-lang/rustlings/commit/135e5d47a7c395aece6f6022117fb20c82f2d3d4)) - add new exercises for generics (#280) ([76be5e4e](https://github.com/rust-lang/rustlings/commit/76be5e4e991160f5fd9093f03ee2ba260e8f7229)) - **ci:** add buildkite config ([b049fa2c](https://github.com/rust-lang/rustlings/commit/b049fa2c84dba0f0c8906ac44e28fd45fba51a71)) - +## 2.2.1 (2020-02-27) -### 2.2.1 (2020-02-27) - -#### Bug Fixes +### Bug Fixes - Re-add cloning the repo to install scripts ([3d9b03c5](https://github.com/rust-lang/rustlings/commit/3d9b03c52b8dc51b140757f6fd25ad87b5782ef5)) -#### Features +### Features - Add clippy lints (#269) ([1e2fd9c9](https://github.com/rust-lang/rustlings/commit/1e2fd9c92f8cd6e389525ca1a999fca4c90b5921)) - - ## 2.2.0 (2020-02-25) -#### Bug Fixes +### Bug Fixes - Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401)) - **docs:** @@ -858,7 +841,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce) - Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf)) -#### Features +### Features - Add variables5.rs exercise (#264) ([0c73609e](https://github.com/rust-lang/rustlings/commit/0c73609e6f2311295e95d6f96f8c747cfc4cba03)) - Show a completion message when watching (#253) ([d25ee55a](https://github.com/rust-lang/rustlings/commit/d25ee55a3205882d35782e370af855051b39c58c)) @@ -868,11 +851,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Added traits exercises (#274 but specifically #216, which originally added this :heart:) ([b559cdd](https://github.com/rust-lang/rustlings/commit/b559cdd73f32c0d0cfc1feda39f82b3e3583df17)) - - ## 2.1.0 (2019-11-27) -#### Bug Fixes +### Bug Fixes - add line numbers in several exercises and hints ([b565c4d3](https://github.com/rust-lang/rustlings/commit/b565c4d3e74e8e110bef201a082fa1302722a7c3)) - **arc1:** Fix some words in the comment ([c42c3b21](https://github.com/rust-lang/rustlings/commit/c42c3b2101df9164c8cd7bb344def921e5ba3e61)) @@ -883,37 +864,33 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **strings2:** update line number in hint ([a09f684f](https://github.com/rust-lang/rustlings/commit/a09f684f05c58d239a6fc59ec5f81c2533e8b820)) - **variables1:** Correct wrong word in comment ([fda5a470](https://github.com/rust-lang/rustlings/commit/fda5a47069e0954f16a04e8e50945e03becb71a5)) -#### Features +### Features - **watch:** show hint while watching ([8143d57b](https://github.com/rust-lang/rustlings/commit/8143d57b4e88c51341dd4a18a14c536042cc009c)) - - ## 2.0.0 (2019-11-12) -#### Bug Fixes +### Bug Fixes - **default:** Clarify the installation procedure ([c371b853](https://github.com/rust-lang/rustlings/commit/c371b853afa08947ddeebec0edd074b171eeaae0)) - **info:** Fix trailing newlines for hints ([795b6e34](https://github.com/rust-lang/rustlings/commit/795b6e348094a898e9227a14f6232f7bb94c8d31)) - **run:** make `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Breaking Changes +### Breaking Changes - Refactor hint system ([9bdb0a12](https://github.com/rust-lang/rustlings/commit/9bdb0a12e45a8e9f9f6a4bd4a9c172c5376c7f60)) - improve `watch` execution mode ([2cdd6129](https://github.com/rust-lang/rustlings/commit/2cdd61294f0d9a53775ee24ad76435bec8a21e60)) - Index exercises by name ([627cdc07](https://github.com/rust-lang/rustlings/commit/627cdc07d07dfe6a740e885e0ddf6900e7ec336b)) - **run:** makes `run` never prompt ([4b265465](https://github.com/rust-lang/rustlings/commit/4b26546589f7d2b50455429482cf1f386ceae8b3)) -#### Features +### Features - **cli:** check for rustc before doing anything ([36a033b8](https://github.com/rust-lang/rustlings/commit/36a033b87a6549c1e5639c908bf7381c84f4f425)) - **hint:** Add test for hint ([ce9fa6eb](https://github.com/rust-lang/rustlings/commit/ce9fa6ebbfdc3e7585d488d9409797285708316f)) - +## 1.5.1 (2019-11-11) -### 1.5.1 (2019-11-11) - -#### Bug Fixes +### Bug Fixes - **errors3:** Update hint ([dcfb427b](https://github.com/rust-lang/rustlings/commit/dcfb427b09585f0193f0a294443fdf99f11c64cb), closes [#185](https://github.com/rust-lang/rustlings/issues/185)) - **if1:** Remove `return` reference ([ad03d180](https://github.com/rust-lang/rustlings/commit/ad03d180c9311c0093e56a3531eec1a9a70cdb45)) @@ -922,11 +899,9 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **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)) - - ## 1.5.0 (2019-11-09) -#### Bug Fixes +### Bug Fixes - **test1:** Rewrite logic ([79a56942](https://github.com/rust-lang/rustlings/commit/79a569422c8309cfc9e4aed25bf4ab3b3859996b)) - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) @@ -942,27 +917,23 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b)) -#### Features +### Features - Add enums exercises ([dc150321](https://github.com/rust-lang/rustlings/commit/dc15032112fc485226a573a18139e5ce928b1755)) - Added exercise for struct update syntax ([1c4c8764](https://github.com/rust-lang/rustlings/commit/1c4c8764ed118740cd4cee73272ddc6cceb9d959)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) - +## 1.4.1 (2019-08-13) -### 1.4.1 (2019-08-13) - -#### Bug Fixes +### Bug Fixes - **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac)) - **option1:** Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) - **test1:** Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - - ## 1.4.0 (2019-07-13) -#### Bug Fixes +### Bug Fixes - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) - **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6)) @@ -971,20 +942,18 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - **cli:** Check if changed exercise file exists before calling verify ([ba85ca3](https://github.com/rust-lang/rustlings/commit/ba85ca32c4cfc61de46851ab89f9c58a28f33c88)) - **structs1:** Fix the irrefutable let pattern warning ([cc6a141](https://github.com/rust-lang/rustlings/commit/cc6a14104d7c034eadc98297eaaa972d09c50b1f)) -#### Features +### Features - **changelog:** Use clog for changelogs ([34e31232](https://github.com/rust-lang/rustlings/commit/34e31232dfddde284a341c9609b33cd27d9d5724)) - **iterators2:** adds iterators2 exercise including config ([9288fccf](https://github.com/rust-lang/rustlings/commit/9288fccf07a2c5043b76d0fd6491e4cf72d76031)) - +## 1.3.0 (2019-06-05) -### 1.3.0 (2019-06-05) - -#### Features +### Features - Adds a simple exercise for structures (#163, @briankung) -#### Bug Fixes +### Bug Fixes - Add Result type signature as it is difficult for new comers to understand Generics and Error all at once. (#157, @veggiemonk) - Rustfmt and whitespace fixes (#161, @eddyp) @@ -993,37 +962,29 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Fix broken link (#164, @HanKruiger) - Remove highlighting and syntect (#167, @komaeda) - +## 1.2.2 (2019-05-07) -### 1.2.2 (2019-05-07) - -#### Bug Fixes +### Bug Fixes - Reverted `--nocapture` flag since it was causing tests to pass unconditionally - +## 1.2.1 (2019-04-22) -### 1.2.1 (2019-04-22) - -#### Bug Fixes +### Bug Fixes - Fix the `--nocapture` feature (@komaeda) - Provide a nicer error message for when you're in the wrong directory - +## 1.2.0 (2019-04-22) -### 1.2.0 (2019-04-22) - -#### Features +### Features - Add errors to exercises that compile without user changes (@yvan-sraka) - Use --nocapture when testing, enabling `println!` when running (@komaeda) - +## 1.1.1 (2019-04-14) -### 1.1.1 (2019-04-14) - -#### Bug fixes +### Bug fixes - Fix permissions on exercise files (@zacanger, #133) - Make installation checks more thorough (@komaeda, 1b3469f236bc6979c27f6e1a04e4138a88e55de3) @@ -1033,9 +994,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Fix links by deleting book version (@diodfr, #142) - Canonicalize paths to fix path matching (@cjpearce, #143) - - -### 1.1.0 (2019-03-20) +## 1.1.0 (2019-03-20) - errors2.rs: update link to Rust book (#124) - Start verification at most recently modified file (#120) @@ -1044,16 +1003,12 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Give a warning when Rustlings isn't run from the right directory (#123) - Verify that rust version is recent enough to install Rustlings (#131) - - -### 1.0.1 (2019-03-06) +## 1.0.1 (2019-03-06) - Adds a way to install Rustlings in one command (`curl -L https://git.io/rustlings | bash`) - Makes `rustlings watch` react to create file events (@shaunbennett, #117) - Reworks the exercise management to use an external TOML file instead of just listing them in the code - - -### 1.0.0 (2019-03-06) +## 1.0.0 (2019-03-06) Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95605f70..7b684d39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ I want to … ## Issues -You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). +You can [open an issue](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: - `cargo --version` diff --git a/Cargo.lock b/Cargo.lock index a61cf39a..dfba4bcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anstream" -version = "0.6.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,73 +19,62 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.20" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -93,9 +82,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -105,9 +94,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -117,24 +106,25 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.6.0", + "bitflags", "crossterm_winapi", + "document-features", "mio", "parking_lot", "rustix", @@ -153,38 +143,41 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "filetime" -version = "0.2.25" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.59.0", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "fsevent-sys" @@ -196,10 +189,32 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.15.1" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -208,28 +223,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] name = "inotify" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 1.3.2", + "bitflags", "inotify-sys", "libc", ] @@ -243,32 +260,23 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -276,80 +284,78 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "285efcf12ef41bec907b3000d5ffaeb54191d4d9d83c0d6157e6cbc2db255e64" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.162" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", - "redox_syscall", -] +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.0.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ - "hermit-abi", "libc", "log", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "notify" -version = "7.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.6.0", - "filetime", + "bitflags", "fsevent-sys", "inotify", "kqueue", @@ -358,39 +364,35 @@ dependencies = [ "mio", "notify-types", "walkdir", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "notify-types" -version = "1.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "instant", + "bitflags", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "os_pipe" -version = "1.2.1" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -398,89 +400,99 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] -name = "redox_syscall" -version = "0.5.7" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] name = "rustix" -version = "0.38.40" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustlings" -version = "6.4.0" +version = "6.5.0" dependencies = [ "anyhow", "clap", "crossterm", "notify", - "os_pipe", "rustix", "rustlings-macros", "serde", "serde_json", + "shlex", "tempfile", - "toml_edit", + "toml", ] [[package]] name = "rustlings-macros" -version = "6.4.0" +version = "6.5.0" dependencies = [ "quote", "serde", - "toml_edit", + "toml", ] -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -497,19 +509,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.214" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -518,30 +546,37 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "signal-hook" -version = "0.3.17" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -549,9 +584,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -560,18 +595,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "strsim" @@ -581,9 +617,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -592,44 +628,67 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "toml_datetime" -version = "0.6.8" +name = "toml" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", "toml_datetime", + "toml_parser", + "toml_writer", "winnow", ] [[package]] -name = "unicode-ident" -version = "1.0.13" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -649,9 +708,61 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] name = "winapi" @@ -671,11 +782,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -685,29 +796,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -720,57 +838,154 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.6.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "memchr", + "wit-bindgen-rust-macro", ] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index ff88de2d..192eeb61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ [workspace] -resolver = "2" exclude = [ "tests/test_exercises", "dev", ] [workspace.package] -version = "6.4.0" +version = "6.5.0" authors = [ "Mo Bitar ", # https://github.com/mo8it "Liv ", # https://github.com/shadows-withal @@ -15,12 +14,12 @@ authors = [ ] repository = "https://github.com/rust-lang/rustlings" license = "MIT" -edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions. -rust-version = "1.80" +edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`. +rust-version = "1.88" [workspace.dependencies] -serde = { version = "1.0.214", features = ["derive"] } -toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] } +serde = { version = "1", features = ["derive"] } +toml = { version = "1", default-features = false, features = ["std", "parse", "serde"] } [package] name = "rustlings" @@ -46,21 +45,21 @@ include = [ ] [dependencies] -anyhow = "1.0.93" -clap = { version = "4.5.20", features = ["derive"] } -crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } -notify = "7.0.0" -os_pipe = "1.2.1" -rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" } -serde_json = "1.0.132" +anyhow = "1" +clap = { version = "4", features = ["derive"] } +crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] } +notify = "8" +rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } +serde_json = "1" serde.workspace = true -toml_edit.workspace = true +shlex = "1" +toml.workspace = true [target.'cfg(not(windows))'.dependencies] -rustix = { version = "0.38.38", default-features = false, features = ["std", "stdio", "termios"] } +rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] -tempfile = "3.14.0" +tempfile = "3" [profile.release] panic = "abort" @@ -84,8 +83,6 @@ infinite_loop = "deny" mem_forget = "deny" dbg_macro = "warn" todo = "warn" -# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102 -needless_option_as_deref = "allow" [lints] workspace = true diff --git a/README.md b/README.md index 9a223670..0ae6265d 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,7 @@ -
+# [Rustlings](https://rustlings.rust-lang.org) πŸ¦€ -# Rustlings πŸ¦€β€οΈ +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) πŸ“šοΈ_ -
+Visit the **website** for a demo, info about setup and more: -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 -``` - -
-If the installation fails… (click to expand) - -- 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) - -
- -### Initialization - -After installing Rustlings, run the following command to initialize the `rustlings/` directory: - -```bash -rustlings init -``` - -
-If the command rustlings can't be found… (click to expand) - -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 - -
- -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/`. -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. - -
-If detecting file changes in the exercises/ directory fails… (click to expand) - -> 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). - -
- -### 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 the selected exercise (you need to _reload/reopen_ its file in your editor afterwards) - -See the footer of the list for all possible keys. - -## Questions? - -If you need any help while doing the exercises and the builtin-hints aren't helpful, feel free to ask in the [_Q&A_ category of the discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question wasn't asked yet πŸ’‘ - -## Third-Party Exercises - -Third-party exercises are a set of exercises maintained by the community. -You can use the same `rustlings` program that you installed with `cargo install rustlings` to run them: - -- [ζ—₯本θͺžη‰ˆ Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings 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 the guide about [third-party exercises](https://github.com/rust-lang/rustlings/blob/main/THIRD_PARTY_EXERCISES.md)! - -## 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. - -## 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) πŸŽ‰ +## ➑️ [rustlings.rust-lang.org](https://rustlings.rust-lang.org) ⬅️ diff --git a/THIRD_PARTY_EXERCISES.md b/THIRD_PARTY_EXERCISES.md deleted file mode 100644 index 62646c5b..00000000 --- a/THIRD_PARTY_EXERCISES.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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 πŸ˜ƒ diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..56878641 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +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"); +} diff --git a/clippy.toml b/clippy.toml index afc9253a..89b0a886 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,15 +1,11 @@ disallowed-types = [ - # Inefficient. Use `.queue(…)` instead. - "crossterm::style::Stylize", - "crossterm::style::styled_content::StyledContent", + { path = "crossterm::style::Stylize", reason = "inefficient, use `.queue(…)` instead" }, + { path = "crossterm::style::styled_content::StyledContent", reason = "inefficient, use `.queue(…)` instead" }, ] disallowed-methods = [ - # Inefficient. Use `.queue(…)` instead. - "crossterm::style::style", - # Use `thread::Builder::spawn` instead and handle the error. - "std::thread::spawn", - "std::thread::Scope::spawn", - # Return `ExitCode` instead. - "std::process::exit", + { 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" }, ] diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 29a557a0..66bc1dfe 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,4 +1,4 @@ -# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`. +# Don't edit the `bin` list manually! It is updated by `cargo dev update`. This comment line will be stripped in `rustlings init`. bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" }, @@ -150,14 +150,14 @@ bin = [ { name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" }, { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, { name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" }, - { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, - { name = "box1_sol", path = "../solutions/19_smart_pointers/box1.rs" }, - { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, - { name = "rc1_sol", path = "../solutions/19_smart_pointers/rc1.rs" }, - { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, - { name = "arc1_sol", path = "../solutions/19_smart_pointers/arc1.rs" }, - { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, - { name = "cow1_sol", path = "../solutions/19_smart_pointers/cow1.rs" }, + { name = "smart_pointers1", path = "../exercises/19_smart_pointers/smart_pointers1.rs" }, + { name = "smart_pointers1_sol", path = "../solutions/19_smart_pointers/smart_pointers1.rs" }, + { name = "smart_pointers2", path = "../exercises/19_smart_pointers/smart_pointers2.rs" }, + { name = "smart_pointers2_sol", path = "../solutions/19_smart_pointers/smart_pointers2.rs" }, + { name = "smart_pointers3", path = "../exercises/19_smart_pointers/smart_pointers3.rs" }, + { name = "smart_pointers3_sol", path = "../solutions/19_smart_pointers/smart_pointers3.rs" }, + { name = "smart_pointers4", path = "../exercises/19_smart_pointers/smart_pointers4.rs" }, + { name = "smart_pointers4_sol", path = "../solutions/19_smart_pointers/smart_pointers4.rs" }, { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, { name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" }, { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, @@ -178,21 +178,21 @@ bin = [ { name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" }, { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, { name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" }, - { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, - { name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" }, - { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, - { name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" }, - { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, - { name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" }, - { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, - { name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" }, - { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, - { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, + { name = "conversions1", path = "../exercises/23_conversions/conversions1.rs" }, + { name = "conversions1_sol", path = "../solutions/23_conversions/conversions1.rs" }, + { name = "conversions2", path = "../exercises/23_conversions/conversions2.rs" }, + { name = "conversions2_sol", path = "../solutions/23_conversions/conversions2.rs" }, + { name = "conversions3", path = "../exercises/23_conversions/conversions3.rs" }, + { name = "conversions3_sol", path = "../solutions/23_conversions/conversions3.rs" }, + { name = "conversions4", path = "../exercises/23_conversions/conversions4.rs" }, + { name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" }, + { name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" }, + { name = "conversions5_sol", path = "../solutions/23_conversions/conversions5.rs" }, ] [package] name = "exercises" -edition = "2021" +edition = "2024" # Don't publish the exercises on crates.io! publish = false diff --git a/exercises/01_variables/README.md b/exercises/01_variables/README.md index 7964ff29..5ba2efca 100644 --- a/exercises/01_variables/README.md +++ b/exercises/01_variables/README.md @@ -1,7 +1,7 @@ # Variables 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 can't change that value. You can make them mutable by adding `mut` in front of the variable name. ## Further information diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index 49db8e9e..cf5620da 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -1,6 +1,6 @@ fn main() { 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. number = 3; diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index 10037f26..ca8493cc 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -19,7 +19,7 @@ mod tests { #[test] fn yummy_food() { - // This means that calling `picky_eater` with the argument "food" should return "Yummy!". + // This means that calling `picky_eater` with the argument "strawberry" should return "Yummy!". assert_eq!(picky_eater("strawberry"), "Yummy!"); } diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 68e1affa..a015c40f 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,11 +1,6 @@ -fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // Array - - // TODO: Create a vector called `v` which contains the exact same elements as in the array `a`. - // Use the vector macro. - // let v = ???; - - (a, v) +fn elems_to_vec(a: i32, b: i32, c: i32) -> Vec { + // TODO: Return a vector containing the elements a, b and c (in this order). + // Use the "vec!" macro. } fn main() { @@ -17,8 +12,11 @@ mod tests { use super::*; #[test] - fn test_array_and_vec_similarity() { - let (a, v) = array_and_vec(); - assert_eq!(a, *v); + fn test_elems_to_vec() { + let (a, b, c) = (2, 7, 12); + let v = elems_to_vec(a, b, c); + assert_eq!(v[0], a); + assert_eq!(v[1], b); + assert_eq!(v[2], c); } } diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index a9be2580..0c996266 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -9,26 +9,6 @@ fn vec_loop(input: &[i32]) -> Vec { output } -fn vec_map_example(input: &[i32]) -> Vec { - // 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 { - // 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() { // You can optionally experiment here. } @@ -43,18 +23,4 @@ mod tests { let ans = vec_loop(&input); 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]); - } } diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 69e5ced7..d42729e1 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,38 +1,28 @@ // Structs contain data, but can also have logic. In this exercise, we have -// defined the `Package` struct, and we want to test some logic attached to it. +// defined the `Fireworks` struct and a couple of functions that work with it. +// Turn these free-standing functions into methods and associated functions +// to express that relationship more clearly in the code. + +#![deny(clippy::use_self)] // practice using the `Self` type #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } +// TODO: Turn this function into an associated function on `Fireworks`. +fn new_fireworks() -> Fireworks { + Fireworks { rockets: 0 } +} - Self { - sender_country, - recipient_country, - weight_in_grams, - } - } +// TODO: Turn this function into a method on `Fireworks`. +fn add_rockets(fireworks: &mut Fireworks, rockets: usize) { + fireworks.rockets += rockets +} - // TODO: Add the correct return type to the function signature. - fn is_international(&self) { - // TODO: Read the tests that use this method to find out when a package - // is considered international. - } - - // TODO: Add the correct return type to the function signature. - fn get_fees(&self, cents_per_gram: u32) { - // TODO: Calculate the package's fees. - } +// TODO: Turn this function into a method on `Fireworks`. +fn start(fireworks: Fireworks) -> String { + "πŸš€".repeat(fireworks.rockets) } fn main() { @@ -44,44 +34,18 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "πŸš€πŸš€πŸš€"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(7); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€"); } } diff --git a/exercises/08_enums/README.md b/exercises/08_enums/README.md index 30d4d91d..b05cb422 100644 --- a/exercises/08_enums/README.md +++ b/exercises/08_enums/README.md @@ -1,10 +1,10 @@ # Enums 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. Rust's 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. ## Further information - [Enums](https://doc.rust-lang.org/book/ch06-00-enums.html) -- [Pattern syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) +- [Pattern syntax](https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html) diff --git a/exercises/09_strings/README.md b/exercises/09_strings/README.md index fa2104cc..df957d58 100644 --- a/exercises/09_strings/README.md +++ b/exercises/09_strings/README.md @@ -1,9 +1,11 @@ # Strings -Rust has two string types, a string slice (`&str`) and an owned string (`String`). +Rust has two string types: a string slice (`&str`) and an owned string (`String`). We're not going to dictate when you should use which one, but we'll show you how to identify and create them, as well as use them. ## Further information -- [Strings](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [Strings (Rust Book)](https://doc.rust-lang.org/book/ch08-02-strings.html) +- [`str` methods](https://doc.rust-lang.org/std/primitive.str.html) +- [`String` methods](https://doc.rust-lang.org/std/string/struct.String.html) diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index 39fce18c..f5e45b0f 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -23,6 +23,7 @@ mod tests { assert_eq!(trim_me("Hello! "), "Hello!"); assert_eq!(trim_me(" What's up!"), "What's up!"); assert_eq!(trim_me(" Hola! "), "Hola!"); + assert_eq!(trim_me("Hi!"), "Hi!"); } #[test] diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index 47307263..43976a64 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -21,8 +21,6 @@ fn main() { placeholder("rust is fun!".to_owned()); - placeholder("nice weather".into()); - placeholder(format!("Interpolation {}", "Station")); // WARNING: This is byte indexing, not character indexing. diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index 99648078..d0c412a8 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -1,8 +1,8 @@ -// This function returns how much icecream there is left in the fridge. +// This function returns how much ice cream there is left in the fridge. // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, -// someone eats it all, so no icecream is left (value 0). Return `None` if +// someone eats it all, so no ice cream is left (value 0). Return `None` if // `hour_of_day` is higher than 23. -fn maybe_icecream(hour_of_day: u16) -> Option { +fn maybe_ice_cream(hour_of_day: u16) -> Option { // TODO: Complete the function body. } @@ -18,19 +18,19 @@ mod tests { fn raw_value() { // TODO: Fix this test. How do you get the value contained in the // Option? - let icecreams = maybe_icecream(12); + let ice_creams = maybe_ice_cream(12); - assert_eq!(icecreams, 5); // Don't change this line. + assert_eq!(ice_creams, 5); // Don't change this line. } #[test] - fn check_icecream() { - assert_eq!(maybe_icecream(0), Some(5)); - assert_eq!(maybe_icecream(9), Some(5)); - assert_eq!(maybe_icecream(18), Some(5)); - assert_eq!(maybe_icecream(22), Some(0)); - assert_eq!(maybe_icecream(23), Some(0)); - assert_eq!(maybe_icecream(24), None); - assert_eq!(maybe_icecream(25), None); + fn check_ice_cream() { + assert_eq!(maybe_ice_cream(0), Some(5)); + assert_eq!(maybe_ice_cream(9), Some(5)); + assert_eq!(maybe_ice_cream(18), Some(5)); + assert_eq!(maybe_ice_cream(22), Some(0)); + assert_eq!(maybe_ice_cream(23), Some(0)); + assert_eq!(maybe_ice_cream(24), None); + assert_eq!(maybe_ice_cream(25), None); } } diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 4cedb512..c97b1d3c 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -9,7 +9,7 @@ fn main() { // TODO: Fix the compiler error by adding something to this match statement. match optional_point { - Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + Some(p) => println!("Coordinates are {},{}", p.x, p.y), _ => panic!("No match!"), } diff --git a/exercises/13_error_handling/README.md b/exercises/13_error_handling/README.md index 3b21f2b7..9b6674bc 100644 --- a/exercises/13_error_handling/README.md +++ b/exercises/13_error_handling/README.md @@ -1,8 +1,8 @@ # Error handling -Most errors aren’t 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. -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. +Most errors aren't 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. +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. ## Further information diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index ba01e54b..144fce7b 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -10,6 +10,7 @@ struct PositiveNonzeroInteger(u64); impl PositiveNonzeroInteger { fn new(value: i64) -> Result { // TODO: This function shouldn't always return an `Ok`. + // Read the tests below to clarify what should be returned. Ok(Self(value as u64)) } } diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 57218351..125779b8 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -6,7 +6,7 @@ // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The `Box` is declared as of type `Box` where +// trait. To do so, the `Box` is declared as of type `Box` where // `Trait` is the trait the compiler looks for on any value used in that // context. For this exercise, that context is the potential errors which // can be returned in a `Result`. diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index b1995e03..73055c79 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -19,15 +19,6 @@ enum ParsePosNonzeroError { ParseInt(ParseIntError), } -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> Self { - Self::Creation(err) - } - - // TODO: Add another error conversion function here. - // fn from_parse_int(???) -> Self { ??? } -} - #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); @@ -44,7 +35,7 @@ impl PositiveNonzeroInteger { // TODO: change this to return an appropriate error instead of panicking // when `parse()` returns an error. let x: i64 = s.parse().unwrap(); - Self::new(x).map_err(ParsePosNonzeroError::from_creation) + Self::new(x).map_err(ParsePosNonzeroError::Creation) } } diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index ca937ed0..8014f0f1 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // TODO: Create an iterator over the array. + // TODO: Create an iterator over the slice. let mut fav_fruits_iterator = todo!(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 6b1eca17..dce09055 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -39,6 +39,8 @@ mod tests { #[test] fn test_success() { assert_eq!(divide(81, 9), Ok(9)); + assert_eq!(divide(81, -1), Ok(-81)); + assert_eq!(divide(i64::MIN, i64::MIN), Ok(1)); } #[test] diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/smart_pointers1.rs similarity index 100% rename from exercises/19_smart_pointers/box1.rs rename to exercises/19_smart_pointers/smart_pointers1.rs diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/smart_pointers2.rs similarity index 100% rename from exercises/19_smart_pointers/rc1.rs rename to exercises/19_smart_pointers/smart_pointers2.rs diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/smart_pointers3.rs similarity index 100% rename from exercises/19_smart_pointers/arc1.rs rename to exercises/19_smart_pointers/smart_pointers3.rs diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/smart_pointers4.rs similarity index 100% rename from exercises/19_smart_pointers/cow1.rs rename to exercises/19_smart_pointers/smart_pointers4.rs diff --git a/exercises/21_macros/README.md b/exercises/21_macros/README.md index 337816d6..de7fb7ba 100644 --- a/exercises/21_macros/README.md +++ b/exercises/21_macros/README.md @@ -10,5 +10,6 @@ of exercises to Rustlings, but is all about learning to write Macros. ## Further information -- [Macros](https://doc.rust-lang.org/book/ch19-06-macros.html) +- [The Rust Book - Macros](https://doc.rust-lang.org/book/ch20-05-macros.html) - [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) diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 7a3cb390..3f23aaee 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -1,7 +1,6 @@ -// 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. -#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<&str> = None; @@ -11,14 +10,16 @@ fn main() { println!("{}", my_option.unwrap()); } + #[rustfmt::skip] let my_arr = &[ -1, -2, -3 -4, -5, -6 ]; println!("My array! Here it is: {my_arr:?}"); - let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5); - println!("This Vec is empty, see? {my_empty_vec:?}"); + let mut my_vec = vec![1, 2, 3, 4, 5]; + my_vec.resize(0, 5); + println!("This Vec is empty, see? {my_vec:?}"); let mut value_a = 45; let mut value_b = 66; diff --git a/exercises/23_conversions/README.md b/exercises/23_conversions/README.md index 619a78c5..fe337331 100644 --- a/exercises/23_conversions/README.md +++ b/exercises/23_conversions/README.md @@ -2,14 +2,14 @@ Rust offers a multitude of ways to convert a value of a given type into another type. -The simplest form of type conversion is a type cast expression. It is denoted with the binary operator `as`. For instance, `println!("{}", 1 + 1.0);` would not compile, since `1` is an integer while `1.0` is a float. However, `println!("{}", 1 as f32 + 1.0)` should compile. The exercise [`using_as`](using_as.rs) tries to cover this. +The simplest form of type conversion is a type cast expression. It is denoted with the binary operator `as`. For instance, `println!("{}", 1 + 1.0);` would not compile, since `1` is an integer while `1.0` is a float. However, `println!("{}", 1 as f32 + 1.0)` should compile. The exercise [`conversions1`](conversions1.rs) tries to cover this. Rust also offers traits that facilitate type conversions upon implementation. These traits can be found under the [`convert`](https://doc.rust-lang.org/std/convert/index.html) module. The traits are the following: -- `From` and `Into` covered in [`from_into`](from_into.rs) -- `TryFrom` and `TryInto` covered in [`try_from_into`](try_from_into.rs) -- `AsRef` and `AsMut` covered in [`as_ref_mut`](as_ref_mut.rs) +- `From` and `Into` covered in [`conversions2`](conversions2.rs) +- `TryFrom` and `TryInto` covered in [`conversions4`](conversions4.rs) +- `AsRef` and `AsMut` covered in [`conversions5`](conversions5.rs) Furthermore, the `std::str` module offers a trait called [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) which helps with converting strings into target types via the `parse` method on strings. If properly implemented for a given type `Person`, then `let p: Person = "Mark,20".parse().unwrap()` should both compile and run without panicking. diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/conversions1.rs similarity index 100% rename from exercises/23_conversions/using_as.rs rename to exercises/23_conversions/conversions1.rs diff --git a/exercises/23_conversions/conversions2.rs b/exercises/23_conversions/conversions2.rs new file mode 100644 index 00000000..491b8073 --- /dev/null +++ b/exercises/23_conversions/conversions2.rs @@ -0,0 +1,54 @@ +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html +// +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. + +struct Celsius(f64); + +struct Fahrenheit(f64); + +impl From for Fahrenheit { + // TODO: Convert Celsius to Fahrenheit. Don't worry about floating-point + // precision. The formula is: F = C * 1.8 + 32 +} + +impl From for Celsius { + // TODO: Convert Fahrenheit to Celsius. +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + + #[test] + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } + } + + #[test] + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } + } +} diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/conversions3.rs similarity index 100% rename from exercises/23_conversions/from_str.rs rename to exercises/23_conversions/conversions3.rs diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/conversions4.rs similarity index 100% rename from exercises/23_conversions/try_from_into.rs rename to exercises/23_conversions/conversions4.rs diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/conversions5.rs similarity index 100% rename from exercises/23_conversions/as_ref_mut.rs rename to exercises/23_conversions/conversions5.rs diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs deleted file mode 100644 index bc2783a3..00000000 --- a/exercises/23_conversions/from_into.rs +++ /dev/null @@ -1,130 +0,0 @@ -// The `From` trait is used for value-to-value conversions. If `From` is -// implemented, an implementation of `Into` is automatically provided. -// You can read more about it in the documentation: -// https://doc.rust-lang.org/std/convert/trait.From.html - -#[derive(Debug)] -struct Person { - name: String, - age: u8, -} - -// We implement the Default trait to use it as a fallback when the provided -// string is not convertible into a `Person` object. -impl Default for Person { - fn default() -> Self { - Self { - name: String::from("John"), - age: 30, - } - } -} - -// TODO: Complete this `From` implementation to be able to parse a `Person` -// out of a string in the form of "Mark,20". -// Note that you'll need to parse the age component into a `u8` with something -// like `"4".parse::()`. -// -// Steps: -// 1. Split the given string on the commas present in it. -// 2. If the split operation returns less or more than 2 elements, return the -// default of `Person`. -// 3. Use the first element from the split operation as the name. -// 4. If the name is empty, return the default of `Person`. -// 5. Parse the second element from the split operation into a `u8` as the age. -// 6. If parsing the age fails, return the default of `Person`. -impl From<&str> for Person { - fn from(s: &str) -> Self {} -} - -fn main() { - // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); - - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); - } - - #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_age() { - let p = Person::from("Mark,twenty"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_comma_and_age() { - let p: Person = Person::from("Mark"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_age() { - let p: Person = Person::from("Mark,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name() { - let p: Person = Person::from(",1"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_age() { - let p: Person = Person::from(","); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_invalid_age() { - let p: Person = Person::from(",one"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma() { - let p: Person = Person::from("Mike,32,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma_and_some_string() { - let p: Person = Person::from("Mike,32,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } -} diff --git a/exercises/README.md b/exercises/README.md index 237f2f1e..24ebd069 100644 --- a/exercises/README.md +++ b/exercises/README.md @@ -9,7 +9,7 @@ | vecs | Β§8.1 | | move_semantics | Β§4.1-2 | | structs | Β§5.1, Β§5.3 | -| enums | Β§6, Β§18.3 | +| enums | Β§6, Β§19.3 | | strings | Β§8.2 | | modules | Β§7 | | hashmaps | Β§8.3 | @@ -22,6 +22,6 @@ | iterators | Β§13.2-4 | | smart_pointers | Β§15, Β§16.3 | | threads | Β§16.1-3 | -| macros | Β§19.5 | -| clippy | Β§21.4 | +| macros | Β§20.5 | +| clippy | Appendix D | | conversions | n/a | diff --git a/oranda.json b/oranda.json deleted file mode 100644 index ecc490b2..00000000 --- a/oranda.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "project": { - "homepage": "https://rustlings.cool", - "repository": "https://github.com/rust-lang/rustlings" - }, - "marketing": { - "analytics": { - "plausible": { - "domain": "rustlings.cool" - } - } - } -} diff --git a/release-hook.sh b/release-hook.sh index 8da5636d..0e955d1f 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -4,13 +4,12 @@ set -e typos -cargo upgrades # Similar to CI cargo clippy -- --deny warnings cargo fmt --all --check -cargo test --workspace --all-targets -cargo run -- dev check --require-solutions +cargo test --workspace +cargo dev check --require-solutions # MSRV -cargo +1.80 run -- dev check --require-solutions +cargo +1.88 dev check --require-solutions diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 3ed56a18..5d123b51 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -16,9 +16,9 @@ include = [ proc-macro = true [dependencies] -quote = "1.0.37" +quote = "1" serde.workspace = true -toml_edit.workspace = true +toml.workspace = true [lints] workspace = true diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index e7055981..6b6af3a1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -21,7 +21,7 @@ get started, here are some notes about how Rustlings operates: final_message = """ We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, don't hesitate to report them on Github. +If you noticed any issues, don't hesitate to report them on GitHub. You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: @@ -318,16 +318,7 @@ of the Rust book to learn more.""" name = "vecs2" dir = "05_vecs" hint = """ -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?""" +Use the `.push()` method on the vector to push new elements to it.""" # MOVE SEMANTICS @@ -426,11 +417,10 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances- name = "structs3" dir = "07_structs" hint = """ -For `is_international`: What makes a package international? Seems related to -the places it goes through right? - -For `get_fees`: This method takes an additional argument, is there a field in -the `Package` struct that this relates to? +Methods and associated functions are both declared in an `impl MyType {}` +block. Methods have a `self`, `&self` or `&mut self` parameter, where `self` +implicitly has the type of the impl block. Associated functions do not have +a `self` parameter. Have a look in The Book to find out more about method implementations: https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" @@ -448,8 +438,14 @@ name = "enums2" dir = "08_enums" test = false hint = """ -You can create enumerations that have different variants with different types -such as anonymous structs, structs, a single string, tuples, no data, etc.""" +Enum variants can be defined using three different forms: struct-, tuple- and +unit-like. Here's an example enum definition, which uses all three forms: + +enum EnumUsingAllVariantForms { + StructLike { named_field: bool }, + TupleLike(bool), + UnitLike, +}""" [[exercises]] name = "enums3" @@ -764,7 +760,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, the implementation can take `mut self` instead. This is possible because the -the value is owned anyway.""" +value is owned anyway.""" [[exercises]] name = "traits3" @@ -963,7 +959,7 @@ a different method that could make your code more compact than using `fold`.""" # SMART POINTERS [[exercises]] -name = "box1" +name = "smart_pointers1" dir = "19_smart_pointers" hint = """ The compiler's message should help: Since we cannot store the value of the @@ -980,7 +976,7 @@ Although the current list is one of integers (`i32`), feel free to change the definition and try other types!""" [[exercises]] -name = "rc1" +name = "smart_pointers2" dir = "19_smart_pointers" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has @@ -997,7 +993,7 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html Unfortunately, Pluto is no longer considered a planet :(""" [[exercises]] -name = "arc1" +name = "smart_pointers3" dir = "19_smart_pointers" test = false hint = """ @@ -1014,7 +1010,7 @@ Book: https://doc.rust-lang.org/book/ch16-00-concurrency.html""" [[exercises]] -name = "cow1" +name = "smart_pointers4" dir = "19_smart_pointers" hint = """ If `Cow` already owns the data, it doesn't need to clone it when `to_mut()` is @@ -1165,20 +1161,26 @@ hint = "No hints this time!" # TYPE CONVERSIONS [[exercises]] -name = "using_as" +name = "conversions1" dir = "23_conversions" hint = """ Use the `as` operator to cast one of the operands in the last line of the `average` function into the expected return type.""" [[exercises]] -name = "from_into" +name = "conversions2" dir = "23_conversions" hint = """ -Follow the steps provided right before the `From` implementation.""" +The formula for converting from Fahrenheit to Celsius is: C = (F - 32) / 1.8 +This can be derived from the first formula: + + F = C * 1.8 + 32 // now subtract 32 on both sides + F - 32 = C * 1.8 // then divide by 1.8 +(F - 32) / 1.8 = C +""" [[exercises]] -name = "from_str" +name = "conversions3" dir = "23_conversions" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, @@ -1195,7 +1197,7 @@ operator in your solution, you might want to look at https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] -name = "try_from_into" +name = "conversions4" dir = "23_conversions" hint = """ Is there an implementation of `TryFrom` in the standard library that can both do @@ -1205,7 +1207,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" [[exercises]] -name = "as_ref_mut" +name = "conversions5" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index 6c6067bc..40eebc16 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -3,20 +3,29 @@ use quote::quote; use serde::Deserialize; #[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: String, +struct ExerciseInfo<'a> { + name: &'a str, + dir: &'a str, } #[derive(Deserialize)] -struct InfoFile { - exercises: Vec, +struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let info_file = include_str!("../info.toml"); - let exercises = toml_edit::de::from_str::(info_file) + // Remove `\r` on Windows + let info_file = String::from_utf8( + include_bytes!("../info.toml") + .iter() + .copied() + .filter(|c| *c != b'\r') + .collect(), + ) + .expect("Failed to parse `info.toml` as UTF8"); + let exercises = toml::de::from_str::(&info_file) .expect("Failed to parse `info.toml`") .exercises; @@ -37,7 +46,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { continue; } - dirs.push(exercise.dir.as_str()); + dirs.push(exercise.dir); *dir_ind = dirs.len() - 1; } diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs index 9057754c..0ea39030 100644 --- a/solutions/01_variables/variables5.rs +++ b/solutions/01_variables/variables5.rs @@ -1,6 +1,6 @@ fn main() { let number = "T-H-R-E-E"; - println!("Spell a number: {}", number); + println!("Spell a number: {number}"); // Using variable shadowing // https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing diff --git a/solutions/03_if/if1.rs b/solutions/03_if/if1.rs index 079c6715..8512a60f 100644 --- a/solutions/03_if/if1.rs +++ b/solutions/03_if/if1.rs @@ -1,9 +1,5 @@ fn bigger(a: i32, b: i32) -> i32 { - if a > b { - a - } else { - b - } + if a > b { a } else { b } } fn main() { diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs index 571644d4..9405f085 100644 --- a/solutions/03_if/if3.rs +++ b/solutions/03_if/if3.rs @@ -2,6 +2,7 @@ fn animal_habitat(animal: &str) -> &str { let identifier = if animal == "crab" { 1 } else if animal == "gopher" { + // Integer, so that every branch has the same type. 2 } else if animal == "snake" { 3 diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs index 55b5676c..f79f4ebf 100644 --- a/solutions/05_vecs/vecs1.rs +++ b/solutions/05_vecs/vecs1.rs @@ -1,10 +1,5 @@ -fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // Array - - // Used the `vec!` macro. - let v = vec![10, 20, 30, 40]; - - (a, v) +fn elems_to_vec(a: i32, b: i32, c: i32) -> Vec { + vec![a, b, c] } fn main() { @@ -16,8 +11,11 @@ mod tests { use super::*; #[test] - fn test_array_and_vec_similarity() { - let (a, v) = array_and_vec(); - assert_eq!(a, *v); + fn test_elems_to_vec() { + let (a, b, c) = (2, 7, 12); + let v = elems_to_vec(a, b, c); + assert_eq!(v[0], a); + assert_eq!(v[1], b); + assert_eq!(v[2], c); } } diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs index 87f7625a..aae71038 100644 --- a/solutions/05_vecs/vecs2.rs +++ b/solutions/05_vecs/vecs2.rs @@ -8,22 +8,6 @@ fn vec_loop(input: &[i32]) -> Vec { output } -fn vec_map_example(input: &[i32]) -> Vec { - // 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 { - // 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() { // You can optionally experiment here. } @@ -38,18 +22,4 @@ mod tests { let ans = vec_loop(&input); 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]); - } } diff --git a/solutions/06_move_semantics/move_semantics4.rs b/solutions/06_move_semantics/move_semantics4.rs index 64fdd9db..1a39d4fc 100644 --- a/solutions/06_move_semantics/move_semantics4.rs +++ b/solutions/06_move_semantics/move_semantics4.rs @@ -4,8 +4,6 @@ fn main() { #[cfg(test)] mod tests { - // TODO: Fix the compiler errors only by reordering the lines in the test. - // Don't add, change or remove any line. #[test] fn move_semantics4() { let mut x = Vec::new(); diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index 3f878cc8..c672bb19 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -1,33 +1,21 @@ +#![deny(clippy::use_self)] // practice using the `Self` type + #[derive(Debug)] -struct Package { - sender_country: String, - recipient_country: String, - weight_in_grams: u32, +struct Fireworks { + rockets: usize, } -impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { - if weight_in_grams < 10 { - // This isn't how you should handle errors in Rust, but we will - // learn about error handling later. - panic!("Can't ship a package with weight below 10 grams"); - } - - Self { - sender_country, - recipient_country, - weight_in_grams, - } +impl Fireworks { + fn new() -> Self { + Self { rockets: 0 } } - fn is_international(&self) -> bool { - // ^^^^^^^ added - self.sender_country != self.recipient_country + fn add_rockets(&mut self, rockets: usize) { + self.rockets += rockets } - fn get_fees(&self, cents_per_gram: u32) -> u32 { - // ^^^^^^ added - self.weight_in_grams * cents_per_gram + fn start(self) -> String { + "πŸš€".repeat(self.rockets) } } @@ -40,44 +28,18 @@ mod tests { use super::*; #[test] - #[should_panic] - fn fail_creating_weightless_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Austria"); + fn start_some_fireworks() { + let f = Fireworks::new(); + assert_eq!(f.start(), ""); - Package::new(sender_country, recipient_country, 5); - } + let mut f = Fireworks::new(); + f.add_rockets(3); + assert_eq!(f.start(), "πŸš€πŸš€πŸš€"); - #[test] - fn create_international_package() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Russia"); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(package.is_international()); - } - - #[test] - fn create_local_package() { - let sender_country = String::from("Canada"); - let recipient_country = sender_country.clone(); - - let package = Package::new(sender_country, recipient_country, 1200); - - assert!(!package.is_international()); - } - - #[test] - fn calculate_transport_fees() { - let sender_country = String::from("Spain"); - let recipient_country = String::from("Spain"); - - let cents_per_gram = 3; - - let package = Package::new(sender_country, recipient_country, 1500); - - assert_eq!(package.get_fees(cents_per_gram), 4500); - assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + let mut f = Fireworks::new(); + f.add_rockets(7); + // We don't use method syntax in the last test to ensure the `start` + // function takes ownership of the fireworks. + assert_eq!(Fireworks::start(f), "πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€πŸš€"); } } diff --git a/solutions/09_strings/strings3.rs b/solutions/09_strings/strings3.rs index a478e62a..ee6b56af 100644 --- a/solutions/09_strings/strings3.rs +++ b/solutions/09_strings/strings3.rs @@ -26,6 +26,7 @@ mod tests { assert_eq!(trim_me("Hello! "), "Hello!"); assert_eq!(trim_me(" What's up!"), "What's up!"); assert_eq!(trim_me(" Hola! "), "Hola!"); + assert_eq!(trim_me("Hi!"), "Hi!"); } #[test] diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 3c69b976..ab976d27 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -15,15 +15,6 @@ fn main() { string("rust is fun!".to_owned()); - // Here, both answers work. - // `.into()` converts a type into an expected type. - // If it is called where `String` is expected, it will convert `&str` to `String`. - string("nice weather".into()); - // But if it is called where `&str` is expected, then `&str` is kept `&str` since no conversion is needed. - // If you remove the `#[allow(…)]` line, then Clippy will tell you to remove `.into()` below since it is a useless conversion. - #[allow(clippy::useless_conversion)] - string_slice("nice weather".into()); - string(format!("Interpolation {}", "Station")); // WARNING: This is byte indexing, not character indexing. diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs index 3a787c43..0a654b88 100644 --- a/solutions/11_hashmaps/hashmaps1.rs +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -1,7 +1,7 @@ // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that // particular fruit is in the basket. You have to put at least 3 different -// types of fruits (e.g apple, banana, mango) in the basket and the total count +// types of fruits (e.g. apple, banana, mango) in the basket and the total count // of all the fruits should be at least 5. use std::collections::HashMap; diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 433b16c3..485bf830 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -60,9 +60,11 @@ England,Spain,1,0"; fn build_scores() { let scores = build_scores_table(RESULTS); - assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"] - .into_iter() - .all(|team_name| scores.contains_key(team_name))); + assert!( + ["England", "France", "Germany", "Italy", "Poland", "Spain"] + .into_iter() + .all(|team_name| scores.contains_key(team_name)) + ); } #[test] diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs index 4d615dd6..a8e6457d 100644 --- a/solutions/12_options/options1.rs +++ b/solutions/12_options/options1.rs @@ -1,8 +1,8 @@ -// This function returns how much icecream there is left in the fridge. +// This function returns how much ice cream there is left in the fridge. // If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, -// someone eats it all, so no icecream is left (value 0). Return `None` if +// someone eats it all, so no ice cream is left (value 0). Return `None` if // `hour_of_day` is higher than 23. -fn maybe_icecream(hour_of_day: u16) -> Option { +fn maybe_ice_cream(hour_of_day: u16) -> Option { match hour_of_day { 0..=21 => Some(5), 22..=23 => Some(0), @@ -21,19 +21,19 @@ mod tests { #[test] fn raw_value() { // Using `unwrap` is fine in a test. - let icecreams = maybe_icecream(12).unwrap(); + let ice_creams = maybe_ice_cream(12).unwrap(); - assert_eq!(icecreams, 5); + assert_eq!(ice_creams, 5); } #[test] - fn check_icecream() { - assert_eq!(maybe_icecream(0), Some(5)); - assert_eq!(maybe_icecream(9), Some(5)); - assert_eq!(maybe_icecream(18), Some(5)); - assert_eq!(maybe_icecream(22), Some(0)); - assert_eq!(maybe_icecream(23), Some(0)); - assert_eq!(maybe_icecream(24), None); - assert_eq!(maybe_icecream(25), None); + fn check_ice_cream() { + assert_eq!(maybe_ice_cream(0), Some(5)); + assert_eq!(maybe_ice_cream(9), Some(5)); + assert_eq!(maybe_ice_cream(18), Some(5)); + assert_eq!(maybe_ice_cream(22), Some(0)); + assert_eq!(maybe_ice_cream(23), Some(0)); + assert_eq!(maybe_ice_cream(24), None); + assert_eq!(maybe_ice_cream(25), None); } } diff --git a/solutions/12_options/options3.rs b/solutions/12_options/options3.rs index 0081eeb2..c918f711 100644 --- a/solutions/12_options/options3.rs +++ b/solutions/12_options/options3.rs @@ -10,7 +10,7 @@ fn main() { // Solution 1: Matching over the `Option` (not `&Option`) but without moving // out of the `Some` variant. match optional_point { - Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y), + Some(ref p) => println!("Coordinates are {},{}", p.x, p.y), // ^^^ added _ => panic!("No match!"), } @@ -18,7 +18,8 @@ fn main() { // Solution 2: Matching over a reference (`&Option`) by added `&` before // `optional_point`. match &optional_point { - Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + //^ added + Some(p) => println!("Coordinates are {},{}", p.x, p.y), _ => panic!("No match!"), } diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs index 0597c8c9..f0e144e7 100644 --- a/solutions/13_error_handling/errors2.rs +++ b/solutions/13_error_handling/errors2.rs @@ -16,7 +16,7 @@ use std::num::ParseIntError; -#[allow(unused_variables)] +#[allow(unused_variables, clippy::question_mark)] fn total_cost(item_quantity: &str) -> Result { let processing_fee = 1; let cost_per_item = 5; diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index c1424eee..93ab3b9d 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -6,7 +6,7 @@ // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The `Box` is declared as of type `Box` where +// trait. To do so, the `Box` is declared as of type `Box` where // `Trait` is the trait the compiler looks for on any value used in that // context. For this exercise, that context is the potential errors which // can be returned in a `Result`. diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index ce18073a..cb49fc5d 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -19,16 +19,6 @@ enum ParsePosNonzeroError { ParseInt(ParseIntError), } -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> Self { - Self::Creation(err) - } - - fn from_parse_int(err: ParseIntError) -> Self { - Self::ParseInt(err) - } -} - // As an alternative solution, implementing the `From` trait allows for the // automatic conversion from a `ParseIntError` into a `ParsePosNonzeroError` // using the `?` operator, without the need to call `map_err`. @@ -59,9 +49,9 @@ impl PositiveNonzeroInteger { fn parse(s: &str) -> Result { // Return an appropriate error instead of panicking when `parse()` // returns an error. - let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?; - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Self::new(x).map_err(ParsePosNonzeroError::from_creation) + let x: i64 = s.parse().map_err(ParsePosNonzeroError::ParseInt)?; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Self::new(x).map_err(ParsePosNonzeroError::Creation) } } diff --git a/solutions/16_lifetimes/lifetimes1.rs b/solutions/16_lifetimes/lifetimes1.rs index ca7b688d..4f56834f 100644 --- a/solutions/16_lifetimes/lifetimes1.rs +++ b/solutions/16_lifetimes/lifetimes1.rs @@ -5,11 +5,7 @@ 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() { diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs index b0f2ef1f..412c6021 100644 --- a/solutions/16_lifetimes/lifetimes2.rs +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -1,14 +1,10 @@ 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() { let string1 = String::from("long string is long"); - // Solution1: You can move `strings2` out of the inner block so that it is + // Solution 1: You can move `strings2` out of the inner block so that it is // not dropped before the print statement. let string2 = String::from("xyz"); let result; @@ -25,7 +21,7 @@ fn main() { { let string2 = String::from("xyz"); result = longest(&string1, &string2); - // Solution2: You can move the print statement into the inner block so + // Solution 2: You can move the print statement into the inner block so // that it is executed before `string2` is dropped. println!("The longest string is '{result}'"); // `string2` dropped here (end of the inner scope). diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs index 93a6008a..79977fa4 100644 --- a/solutions/18_iterators/iterators1.rs +++ b/solutions/18_iterators/iterators1.rs @@ -10,9 +10,9 @@ fn main() { mod tests { #[test] fn iterators() { - let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = &["banana", "custard apple", "avocado", "peach", "raspberry"]; - // Create an iterator over the array. + // Create an iterator over the slice. let mut fav_fruits_iterator = my_fav_fruits.iter(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs index 11aa1ec8..1d5d67f2 100644 --- a/solutions/18_iterators/iterators3.rs +++ b/solutions/18_iterators/iterators3.rs @@ -52,6 +52,8 @@ mod tests { #[test] fn test_success() { assert_eq!(divide(81, 9), Ok(9)); + assert_eq!(divide(81, -1), Ok(-81)); + assert_eq!(divide(i64::MIN, i64::MIN), Ok(1)); } #[test] diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/smart_pointers1.rs similarity index 100% rename from solutions/19_smart_pointers/box1.rs rename to solutions/19_smart_pointers/smart_pointers1.rs diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/smart_pointers2.rs similarity index 98% rename from solutions/19_smart_pointers/rc1.rs rename to solutions/19_smart_pointers/smart_pointers2.rs index c0a41abf..edf40ebe 100644 --- a/solutions/19_smart_pointers/rc1.rs +++ b/solutions/19_smart_pointers/smart_pointers2.rs @@ -63,12 +63,10 @@ mod tests { println!("reference count = {}", Rc::strong_count(&sun)); // 7 references saturn.details(); - // TODO let uranus = Planet::Uranus(Rc::clone(&sun)); println!("reference count = {}", Rc::strong_count(&sun)); // 8 references uranus.details(); - // TODO let neptune = Planet::Neptune(Rc::clone(&sun)); println!("reference count = {}", Rc::strong_count(&sun)); // 9 references neptune.details(); diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/smart_pointers3.rs similarity index 100% rename from solutions/19_smart_pointers/arc1.rs rename to solutions/19_smart_pointers/smart_pointers3.rs diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/smart_pointers4.rs similarity index 100% rename from solutions/19_smart_pointers/cow1.rs rename to solutions/19_smart_pointers/smart_pointers4.rs diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs index df35be4d..9d574f89 100644 --- a/solutions/21_macros/macros3.rs +++ b/solutions/21_macros/macros3.rs @@ -1,4 +1,4 @@ -// Added the attribute `macro_use` attribute. +// Added the `macro_use` attribute. #[macro_use] mod macros { macro_rules! my_macro { diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs index b7eaa570..8fc26704 100644 --- a/solutions/22_clippy/clippy3.rs +++ b/solutions/22_clippy/clippy3.rs @@ -1,6 +1,5 @@ use std::mem; -#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<&str> = None; @@ -11,21 +10,22 @@ fn main() { } // A comma was missing. + #[rustfmt::skip] let my_arr = &[ -1, -2, -3, -4, -5, -6, ]; - println!("My array! Here it is: {:?}", my_arr); + println!("My array! Here it is: {my_arr:?}"); - let mut my_empty_vec = vec![1, 2, 3, 4, 5]; + let mut my_vec = vec![1, 2, 3, 4, 5]; // `resize` mutates a vector instead of returning a new one. // `resize(0, …)` clears a vector, so it is better to use `clear`. - my_empty_vec.clear(); - println!("This Vec is empty, see? {my_empty_vec:?}"); + my_vec.clear(); + println!("This Vec is empty, see? {my_vec:?}"); let mut value_a = 45; let mut value_b = 66; // Use `mem::swap` to correctly swap two values. mem::swap(&mut value_a, &mut value_b); - println!("value a: {}; value b: {}", value_a, value_b); + println!("value a: {value_a}; value b: {value_b}"); } diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/conversions1.rs similarity index 100% rename from solutions/23_conversions/using_as.rs rename to solutions/23_conversions/conversions1.rs diff --git a/solutions/23_conversions/conversions2.rs b/solutions/23_conversions/conversions2.rs new file mode 100644 index 00000000..c8580f6b --- /dev/null +++ b/solutions/23_conversions/conversions2.rs @@ -0,0 +1,57 @@ +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html +// +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. + +struct Celsius(f64); + +struct Fahrenheit(f64); + +impl From for Fahrenheit { + fn from(Celsius(celsius): Celsius) -> Self { + Fahrenheit(celsius * 1.8 + 32.0) + } +} + +impl From for Celsius { + fn from(Fahrenheit(fahrenheit): Fahrenheit) -> Self { + Celsius((fahrenheit - 32.0) / 1.8) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + + #[test] + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } + } + + #[test] + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } + } +} diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/conversions3.rs similarity index 100% rename from solutions/23_conversions/from_str.rs rename to solutions/23_conversions/conversions3.rs diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/conversions4.rs similarity index 95% rename from solutions/23_conversions/try_from_into.rs rename to solutions/23_conversions/conversions4.rs index ee802eb0..15257cf2 100644 --- a/solutions/23_conversions/try_from_into.rs +++ b/solutions/23_conversions/conversions4.rs @@ -52,13 +52,12 @@ impl TryFrom<&[i16]> for Color { type Error = IntoColorError; fn try_from(slice: &[i16]) -> Result { - // Check the length. - if slice.len() != 3 { - return Err(IntoColorError::BadLen); + if let &[red, green, blue] = slice { + // Reuse the implementation for a tuple. + Self::try_from((red, green, blue)) + } else { + Err(IntoColorError::BadLen) } - - // Reuse the implementation for a tuple. - Self::try_from((slice[0], slice[1], slice[2])) } } diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/conversions5.rs similarity index 100% rename from solutions/23_conversions/as_ref_mut.rs rename to solutions/23_conversions/conversions5.rs diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/from_into.rs deleted file mode 100644 index cec23cb4..00000000 --- a/solutions/23_conversions/from_into.rs +++ /dev/null @@ -1,136 +0,0 @@ -// The `From` trait is used for value-to-value conversions. If `From` is -// implemented, an implementation of `Into` is automatically provided. -// You can read more about it in the documentation: -// https://doc.rust-lang.org/std/convert/trait.From.html - -#[derive(Debug)] -struct Person { - name: String, - age: u8, -} - -// We implement the Default trait to use it as a fallback when the provided -// string is not convertible into a `Person` object. -impl Default for Person { - fn default() -> Self { - Self { - name: String::from("John"), - age: 30, - } - } -} - -impl From<&str> for Person { - fn from(s: &str) -> Self { - let mut split = s.split(','); - let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { - // ^^^^ there should be no third element - return Self::default(); - }; - - if name.is_empty() { - return Self::default(); - } - - let Ok(age) = age.parse() else { - return Self::default(); - }; - - Self { - name: name.into(), - age, - } - } -} - -fn main() { - // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); - - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); - } - - #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_age() { - let p = Person::from("Mark,twenty"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_comma_and_age() { - let p: Person = Person::from("Mark"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_age() { - let p: Person = Person::from("Mark,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name() { - let p: Person = Person::from(",1"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_age() { - let p: Person = Person::from(","); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_missing_name_and_invalid_age() { - let p: Person = Person::from(",one"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma() { - let p: Person = Person::from("Mike,32,"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_trailing_comma_and_some_string() { - let p: Person = Person::from("Mike,32,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } -} diff --git a/solutions/quizzes/quiz2.rs b/solutions/quizzes/quiz2.rs index 58cbe4e2..8b073b18 100644 --- a/solutions/quizzes/quiz2.rs +++ b/solutions/quizzes/quiz2.rs @@ -62,8 +62,8 @@ mod tests { // Import `transformer`. use super::my_module::transformer; - use super::my_module::transformer_iter; use super::Command; + use super::my_module::transformer_iter; #[test] fn it_works() { diff --git a/src/app_state.rs b/src/app_state.rs index 1c623075..8952db75 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,11 +1,10 @@ -use anyhow::{bail, Context, Error, Result}; -use crossterm::{cursor, terminal, QueueableCommand}; +use anyhow::{Context, Error, Result, bail}; +use crossterm::{QueueableCommand, cursor, terminal}; use std::{ collections::HashSet, - env, fs::{File, OpenOptions}, io::{Read, Seek, StdoutLock, Write}, - path::{Path, MAIN_SEPARATOR_STR}, + path::{MAIN_SEPARATOR_STR, Path}, process::{Command, Stdio}, sync::{ atomic::{AtomicUsize, Ordering::Relaxed}, @@ -17,6 +16,7 @@ use std::{ use crate::{ clear_terminal, cmd::CmdRunner, + editor::{Editor, EditorJoinHandle}, embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, @@ -53,23 +53,25 @@ pub enum CheckProgress { pub struct AppState { current_exercise_ind: usize, exercises: Vec, - // Caches the number of done exercises to avoid iterating over all exercises every time. - n_done: u16, - final_message: String, + // Cache the number of done exercises to avoid iterating over all exercises every time. + n_done: u32, + final_message: &'static str, state_file: File, // Preallocated buffer for reading and writing the state file. file_buf: Vec, official_exercises: bool, cmd_runner: CmdRunner, - // Running in VS Code. - vs_code: bool, + emit_file_links: bool, + editor: Option, } impl AppState { pub fn new( exercise_infos: Vec, - final_message: String, + final_message: &'static str, base_url: Option, + editor: Option, + vs_code_term: bool, ) -> Result<(Self, StateFileStatus)> { let cmd_runner = CmdRunner::build()?; let mut state_file = OpenOptions::new() @@ -89,43 +91,40 @@ impl AppState { let mut exercises = exercise_infos .into_iter() .map(|exercise_info| { - // Leaking to be able to borrow in the watch mode `Table`. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - let path = exercise_info.path().leak(); - let name = exercise_info.name.leak(); - let dir = exercise_info.dir.map(|dir| &*dir.leak()); - let mut hint = exercise_info.hint.leak().trim_ascii(); - - if let Some(replacer) = &url_replacer { - hint = replacer.replace(hint).leak(); - } + let hint = if let Some(replacer) = &url_replacer { + replacer.replace(exercise_info.hint.trim_ascii()).leak() + } else { + exercise_info.hint.trim_ascii() + }; let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; - if let Some(dir) = dir { + if let Some(dir) = exercise_info.dir { canonical_path = String::with_capacity( - 2 + dir_canonical_path.len() + dir.len() + name.len(), + 2 + dir_canonical_path.len() + dir.len() + exercise_info.name.len(), ); canonical_path.push_str(dir_canonical_path); canonical_path.push_str(MAIN_SEPARATOR_STR); canonical_path.push_str(dir); } else { - canonical_path = - String::with_capacity(1 + dir_canonical_path.len() + name.len()); + canonical_path = String::with_capacity( + 1 + dir_canonical_path.len() + exercise_info.name.len(), + ); canonical_path.push_str(dir_canonical_path); } canonical_path.push_str(MAIN_SEPARATOR_STR); - canonical_path.push_str(name); + canonical_path.push_str(exercise_info.name); canonical_path.push_str(".rs"); canonical_path }); Exercise { - dir, - name, - path, + name: exercise_info.name, + dir: exercise_info.dir, + // Leaking for `Editor::open`. + // Leaking is fine since the app state exists until the end of the program. + path: exercise_info.path().leak(), canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, @@ -190,45 +189,40 @@ impl AppState { file_buf, official_exercises: !Path::new("info.toml").exists(), cmd_runner, - vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), + // VS Code has its own file link handling + emit_file_links: !vs_code_term, + editor, }; Ok((slf, state_file_status)) } - #[inline] pub fn current_exercise_ind(&self) -> usize { self.current_exercise_ind } - #[inline] pub fn exercises(&self) -> &[Exercise] { &self.exercises } - #[inline] - pub fn n_done(&self) -> u16 { + pub fn n_done(&self) -> u32 { self.n_done } - #[inline] - pub fn n_pending(&self) -> u16 { - self.exercises.len() as u16 - self.n_done + pub fn n_pending(&self) -> u32 { + self.exercises.len() as u32 - self.n_done } - #[inline] pub fn current_exercise(&self) -> &Exercise { &self.exercises[self.current_exercise_ind] } - #[inline] pub fn cmd_runner(&self) -> &CmdRunner { &self.cmd_runner } - #[inline] - pub fn vs_code(&self) -> bool { - self.vs_code + pub fn emit_file_links(&self) -> bool { + self.emit_file_links } // Write the state file. @@ -324,7 +318,7 @@ impl AppState { } // Official exercises: Dump the original file from the binary. - // Third-party exercises: Reset the exercise file with `git stash`. + // Community exercises: Reset the exercise file with `git stash`. fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> { if self.official_exercises { return EMBEDDED_FILES @@ -352,12 +346,10 @@ impl AppState { Ok(()) } - pub fn reset_current_exercise(&mut self) -> Result<&'static str> { + pub fn reset_current_exercise(&mut self) -> Result<()> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.path)?; - - Ok(exercise.path) + self.reset(self.current_exercise_ind, exercise.path) } // Reset the exercise by index and return its name. @@ -394,7 +386,7 @@ impl AppState { } /// Official exercises: Dump the solution file from the binary and return its path. - /// Third-party exercises: Check if a solution file exists and return its path in that case. + /// Community exercises: Check if a solution file exists and return its path in that case. pub fn current_solution_path(&self) -> Result> { if cfg!(debug_assertions) { return Ok(None); @@ -436,32 +428,34 @@ impl AppState { 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; - }; + .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; - }; + 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, - }; + 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; + if exercise_progress_sender + .send((exercise_ind, progress)) + .is_err() + { + break; + } } }) .context("Failed to spawn a thread to check all exercises")?; @@ -564,7 +558,7 @@ impl AppState { pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> { clear_terminal(stdout)?; - stdout.write_all(FENISH_LINE.as_bytes())?; + stdout.write_all(FINISH_LINE.as_bytes())?; let final_message = self.final_message.trim_ascii(); if !final_message.is_empty() { @@ -574,29 +568,51 @@ impl AppState { Ok(()) } + + pub fn open_editor(&mut self) -> Result { + if let Some(editor) = self.editor.take() { + return editor.open(self.current_exercise_ind, self.current_exercise().path); + } + + Ok(EditorJoinHandle::default()) + } + + pub fn join_editor_handle(&mut self, handle: EditorJoinHandle) -> Result<()> { + self.editor = handle.join()?; + + Ok(()) + } + + pub fn close_editor(&mut self) -> Result<()> { + if let Some(editor) = &mut self.editor { + editor.close()?; + } + + Ok(()) + } } const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | +const FINISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the finish line! | +-------------------------- ------------------------+ - \\/\x1b[31m - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ - β–‘β–‘β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ - β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ - β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–‘β–‘ β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ - β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m + \\/\x1b[31m + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–’β–’β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–’β–’ + β–’β–’β–’β–’β–‘β–‘β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’β–‘β–‘β–’β–’β–’β–’ + β–“β–“β–“β–“β–“β–“β–“β–“ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–ˆβ–ˆ β–“β–“ β–“β–“β–“β–“β–“β–“β–“β–“ + β–’β–’β–’β–’ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–ˆβ–ˆβ–ˆβ–ˆ β–’β–’ β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ β–’β–’ + β–’β–’ β–’β–’ β–’β–’ β–’β–’\x1b[0m "; @@ -606,8 +622,8 @@ mod tests { fn dummy_exercise() -> Exercise { Exercise { - dir: None, name: "0", + dir: None, path: "exercises/0.rs", canonical_path: None, test: false, @@ -623,12 +639,13 @@ mod tests { current_exercise_ind: 0, exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], n_done: 0, - final_message: String::new(), + final_message: "", state_file: tempfile::tempfile().unwrap(), file_buf: Vec::new(), official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), - vs_code: false, + emit_file_links: true, + editor: None, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 8d417ffa..9297da82 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -38,7 +38,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"exercises/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -56,7 +56,7 @@ pub fn append_bins( buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(b"solutions/"); - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); buf.push(b'/'); } @@ -74,13 +74,13 @@ pub fn updated_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); - updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); + updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[..bins_start_ind]); append_bins( &mut updated_cargo_toml, exercise_infos, exercise_path_prefix, ); - updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); + updated_cargo_toml.extend_from_slice(¤t_cargo_toml.as_bytes()[bins_end_ind..]); Ok(updated_cargo_toml) } @@ -106,19 +106,19 @@ mod tests { fn test_bins() { let exercise_infos = [ ExerciseInfo { - name: String::from("1"), + name: "1", dir: None, test: true, strict_clippy: true, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ExerciseInfo { - name: String::from("2"), - dir: Some(String::from("d")), + name: "2", + dir: Some("d"), test: false, strict_clippy: false, - hint: String::new(), + hint: "", skip_check_unsolved: false, }, ]; @@ -134,7 +134,14 @@ mod tests { ); assert_eq!( - updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(), + updated_cargo_toml( + &exercise_infos, + "abc\n\ + bin = [xxx]\n\ + 123", + b"../" + ) + .unwrap(), br#"abc bin = [ { name = "1", path = "../exercises/1.rs" }, diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..153994be --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,57 @@ +use clap::{Parser, Subcommand}; + +use crate::dev::DevCommand; + +/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code +#[derive(Parser)] +#[command(version)] +pub struct Args { + #[command(subcommand)] + pub command: Option, + /// Disable automatic opening of the current file in VS Code or Zellij. + /// Ignores `--edit-cmd` + #[arg(long)] + pub no_editor: bool, + /// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`. + /// The command is not allowed to block (e.g. `vim`). + /// It should communicate with an editor in a different process. + /// `EDIT_CMD` can contain arguments like `--edit-cmd "PROGRAM -x --arg1"`. + /// The current exercise's path is added by Rustlings as the last argument. + /// `--edit-cmd` is ignored in VS Code. + /// + /// Example: `--edit-cmd "code"` (default behavior if running in a VS Code terminal) + #[arg(long)] + pub edit_cmd: Option, + /// Manually run the current exercise using `r` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes + #[arg(long)] + pub manual_run: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Initialize the official Rustlings exercises + Init, + /// Run a single exercise. + /// Runs the next pending exercise if the exercise name is not specified + Run { + /// The name of the exercise + name: Option, + }, + /// Check all the exercises, marking them as done or pending accordingly + CheckAll, + /// Reset a single exercise + Reset { + /// The name of the exercise + name: String, + }, + /// Show a hint. + /// Shows the hint of the next pending exercise if the exercise name is not specified + Hint { + /// The name of the exercise + name: Option, + }, + /// Commands for developing (community) Rustlings exercises + #[command(subcommand)] + Dev(DevCommand), +} diff --git a/src/cmd.rs b/src/cmd.rs index 30f988a6..6442e449 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,7 +1,7 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use serde::Deserialize; use std::{ - io::Read, + io::{Read, pipe}, path::PathBuf, process::{Command, Stdio}, }; @@ -17,7 +17,7 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> }; let mut handle = if let Some(output) = output { - let (mut reader, writer) = os_pipe::pipe().with_context(|| { + let (mut reader, writer) = pipe().with_context(|| { format!("Failed to create a pipe to run the command `{description}``") })?; @@ -126,7 +126,6 @@ pub struct CargoSubcommand<'out> { } impl CargoSubcommand<'_> { - #[inline] pub fn args<'arg, I>(&mut self, args: I) -> &mut Self where I: IntoIterator, @@ -136,7 +135,6 @@ impl CargoSubcommand<'_> { } /// The boolean in the returned `Result` is true if the command's exit status is success. - #[inline] pub fn run(self, description: &str) -> Result { run_cmd(self.cmd, description, self.output) } diff --git a/src/dev.rs b/src/dev.rs index 8af40d69..f2be6066 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use clap::Subcommand; use std::path::PathBuf; @@ -7,8 +7,8 @@ mod new; mod update; #[derive(Subcommand)] -pub enum DevCommands { - /// Create a new project for third-party Rustlings exercises +pub enum DevCommand { + /// Create a new project for community exercises New { /// The path to create the project in path: PathBuf, @@ -26,7 +26,7 @@ pub enum DevCommands { Update, } -impl DevCommands { +impl DevCommand { pub fn run(self) -> Result<()> { match self { Self::New { path, no_git } => { diff --git a/src/dev/check.rs b/src/dev/check.rs index 956c2be2..16d04563 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,8 +1,8 @@ -use anyhow::{anyhow, bail, Context, Error, Result}; +use anyhow::{Context, Error, Result, anyhow, bail}; use std::{ cmp::Ordering, collections::HashSet, - fs::{self, read_dir, OpenOptions}, + fs::{self, OpenOptions, read_dir}, io::{self, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, @@ -10,11 +10,12 @@ use std::{ }; use crate::{ - cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, - cmd::CmdRunner, - exercise::{RunnableExercise, OUTPUT_CAPACITY}, - info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, + cargo_toml::{BINS_BUFFER_CAPACITY, append_bins, bins_start_end_ind}, + cmd::CmdRunner, + exercise::{OUTPUT_CAPACITY, RunnableExercise}, + info_file::{ExerciseInfo, InfoFile}, + term::ProgressCounter, }; const MAX_N_EXERCISES: usize = 999; @@ -42,10 +43,14 @@ fn check_cargo_toml( if old_bins != new_bins { if cfg!(debug_assertions) { - bail!("The file `dev/Cargo.toml` is outdated. Run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again"); + bail!( + "The file `dev/Cargo.toml` is outdated. Run `cargo dev update` to update it. Then run `cargo run -- dev check` again" + ); } - bail!("The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again"); + bail!( + "The file `Cargo.toml` is outdated. Run `rustlings dev update` to update it. Then run `rustlings dev check` again" + ); } Ok(()) @@ -58,18 +63,20 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut file_buf = String::with_capacity(1 << 14); for exercise_info in &info_file.exercises { - let name = exercise_info.name.as_str(); + let name = exercise_info.name; if name.is_empty() { bail!("Found an empty exercise name in `info.toml`"); } if name.len() > MAX_EXERCISE_NAME_LEN { - bail!("The length of the exercise name `{name}` is bigger than the maximum {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) { bail!("Char `{c}` in the exercise name `{name}` is not allowed"); } - if let Some(dir) = &exercise_info.dir { + if let Some(dir) = exercise_info.dir { if dir.is_empty() { bail!("The exercise `{name}` has an empty dir name in `info.toml`"); } @@ -79,7 +86,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { } if exercise_info.hint.trim_ascii().is_empty() { - 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"); + 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" + ); } if !names.insert(name) { @@ -96,20 +105,30 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { .with_context(|| format!("Failed to read the file {path}"))?; if !file_buf.contains("fn main()") { - bail!("The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors"); + bail!( + "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") { - bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."); + bail!( + "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 { 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"); + 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"); + bail!( + "The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file" + ); } file_buf.clear(); @@ -125,7 +144,10 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { // Only one level of directory nesting is allowed. fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> Result<()> { let unexpected_file = |path: &Path| { - 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()) + 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() + ) }; for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? { @@ -154,7 +176,10 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R let path = entry.path(); if !entry.file_type().unwrap().is_file() { - bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display()); + bail!( + "Found `{}` but expected only files. Only one level of exercise nesting is allowed", + path.display() + ); } let file_name = path.file_name().unwrap(); @@ -189,16 +214,13 @@ fn check_exercises_unsolved( Some( thread::Builder::new() .spawn(|| exercise_info.run_exercise(None, cmd_runner)) - .map(|handle| (exercise_info.name.as_str(), handle)), + .map(|handle| (exercise_info.name, handle)), ) }) .collect::, _>>() .context("Failed to spawn a thread to check if an exercise is already solved")?; - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; + let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?; for (exercise_name, handle) in handles { let Ok(result) = handle.join() else { @@ -207,25 +229,31 @@ fn check_exercises_unsolved( match result { Ok(true) => { - bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",) + bail!( + "The exercise {exercise_name} is already solved.\n\ + {SKIP_CHECK_UNSOLVED_HINT}", + ) } Ok(false) => (), Err(e) => return Err(e), } - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; + progress_counter.increment()?; } - stdout.write_all(b"\n")?; Ok(()) } fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { - Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), - Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), + Ordering::Less => bail!( + "`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\n\ + 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 => (), } @@ -287,15 +315,12 @@ fn check_solutions( fmt_cmd .arg("--check") .arg("--edition") - .arg("2021") + .arg("2024") .arg("--color") .arg("always") .stdin(Stdio::null()); - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; + 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 { @@ -312,7 +337,7 @@ fn check_solutions( } SolutionCheck::MissingOptional => (), SolutionCheck::RunFailure { output } => { - stdout.write_all(b"\n\n")?; + drop(progress_counter); stdout.write_all(&output)?; bail!( "Running the solution of the exercise {} failed with the error above", @@ -322,22 +347,21 @@ fn check_solutions( SolutionCheck::Err(e) => return Err(e), } - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; + progress_counter.increment()?; } - stdout.write_all(b"\n")?; + let n_solutions = sol_paths.len(); 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 !fmt_cmd - .status() - .context("Failed to run `rustfmt` on all solution files")? - .success() + if n_solutions > 0 + && !fmt_cmd + .status() + .context("Failed to run `rustfmt` on all solution files")? + .success() { bail!("Some solutions aren't formatted. Run `rustfmt` on them"); } @@ -353,7 +377,7 @@ pub fn check(require_solutions: bool) -> Result<()> { } if cfg!(debug_assertions) { - // A hack to make `cargo run -- dev check` work when developing Rustlings. + // A hack to make `cargo dev check` work when developing Rustlings. check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?; } else { check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; diff --git a/src/dev/new.rs b/src/dev/new.rs index 154cd224..7c72a6b7 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use std::{ env::set_current_dir, fs::{self, create_dir}, @@ -6,7 +6,7 @@ use std::{ process::Command, }; -use crate::{init::RUST_ANALYZER_TOML, CURRENT_FORMAT_VERSION}; +use crate::{CURRENT_FORMAT_VERSION, init::RUST_ANALYZER_TOML}; // Create a directory relative to the current directory and print its path. fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { @@ -55,7 +55,9 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { write_rel_file( "info.toml", &dir_path_str, - format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"), + format!( + "{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}" + ), )?; write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?; @@ -76,18 +78,17 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { Ok(()) } -pub const GITIGNORE: &[u8] = b".rustlings-state.txt -Cargo.lock +pub const GITIGNORE: &[u8] = b"Cargo.lock target/ .vscode/ !.vscode/extensions.json "; const INFO_FILE_BEFORE_FORMAT_VERSION: &str = - "# The format version is an indicator of the compatibility of third-party exercises with the + "# The format version is an indicator of the compatibility of community exercises with 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 third-party +# In case Rustlings makes an unavoidable breaking change to the expected format of community # 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. format_version = "; @@ -95,7 +96,7 @@ format_version = "; const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#" # Optional multi-line message to be shown to users when just starting with the exercises. -welcome_message = """Welcome to these third-party Rustlings exercises.""" +welcome_message = """Welcome to these community Rustlings 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""" @@ -130,7 +131,7 @@ bin = [] [package] name = "exercises" -edition = "2021" +edition = "2024" # Don't publish the exercises on crates.io! publish = false @@ -139,7 +140,7 @@ publish = false const README: &str = "# Rustlings πŸ¦€ -Welcome to these third-party Rustlings exercises πŸ˜ƒ +Welcome to these community Rustlings exercises πŸ˜ƒ First, [install Rustlings using the official instructions](https://github.com/rust-lang/rustlings) βœ… diff --git a/src/dev/update.rs b/src/dev/update.rs index 6de3c8f7..e0855a0e 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -28,7 +28,7 @@ pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; if cfg!(debug_assertions) { - // A hack to make `cargo run -- dev update` work when developing Rustlings. + // A hack to make `cargo dev update` work when developing Rustlings. update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../") .context("Failed to update the file `dev/Cargo.toml`")?; diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 00000000..3c189c78 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,144 @@ +use std::{ + borrow::Cow, + env, + process::{Command, Stdio}, + thread::{self, JoinHandle}, +}; + +use anyhow::{Context, Result, bail}; +use shlex::Shlex; + +mod zellij; + +fn run_cmd(cmd: &mut Command) -> Result> { + let output = cmd + .stdin(Stdio::null()) + .output() + .with_context(|| format!("Failed to run the command {cmd:?}"))?; + + if !output.status.success() { + bail!( + "The command {cmd:?} didn't run successfully\n\n\ + stdout:\n{}\n\n\ + stderr:\n{}", + str::from_utf8(&output.stdout).unwrap_or_default(), + str::from_utf8(&output.stderr).unwrap_or_default(), + ); + } + + Ok(output.stdout) +} + +fn program_exists(program: &str) -> bool { + Command::new(program) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} + +pub enum Editor { + Cmd(Cow<'static, str>, Vec), + Zellij(Option<(String, u32, usize)>), +} + +impl Editor { + pub fn new(cmd: Option, vs_code_term: bool) -> Result> { + if vs_code_term { + for program in ["code", "codium"] { + if program_exists(program) { + return Ok(Some(Self::Cmd(Cow::Borrowed(program), Vec::new()))); + } + } + } + + if let Some(cmd) = cmd { + let shlex = &mut Shlex::new(&cmd); + let program = shlex.next().context("Program missing in `--edit-cmd`")?; + let args = shlex.collect(); + if shlex.had_error { + bail!("Failed to parse the command in `--edit-cmd`"); + } + return Ok(Some(Self::Cmd(Cow::Owned(program), args))); + } + + if env::var_os("ZELLIJ").is_some() && program_exists("zellij") { + return Ok(Some(Self::Zellij(None))); + } + + Ok(None) + } + + pub fn open( + mut self, + exercise_ind: usize, + exercise_path: &'static str, + ) -> Result { + let handle = thread::Builder::new() + .spawn(move || { + match &mut self { + Editor::Cmd(program, args) => { + run_cmd(Command::new(&**program).args(args).arg(exercise_path))?; + } + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, pane_id, open_exercise_ind)) = open_pane { + if *open_exercise_ind == exercise_ind { + if zellij::pane_open(*pane_id)? { + return Ok(self); + } + } else { + zellij::close_pane(pane_id_str)?; + } + } + + let stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("edit") + .arg(exercise_path), + )?; + + let (pane_id_str, pane_id) = zellij::parse_pane_id(&stdout) + .context("Failed to parse the ID of the new Zellij pane")?; + + *open_pane = Some((pane_id_str, pane_id, exercise_ind)); + } + } + + Ok(self) + }) + .context("Failed to spawn a thread to open the editor")?; + + Ok(EditorJoinHandle(Some(handle))) + } + + pub fn close(&mut self) -> Result<()> { + match self { + Editor::Cmd(_, _) => (), + Editor::Zellij(open_pane) => { + if let Some((pane_id_str, _, _)) = open_pane.take() { + zellij::close_pane(&pane_id_str)?; + } + } + } + + Ok(()) + } +} + +#[must_use] +#[derive(Default)] +pub struct EditorJoinHandle(Option>>); + +impl EditorJoinHandle { + pub fn join(self) -> Result> { + if let Some(handle) = self.0 { + let editor = handle.join().unwrap()?; + return Ok(Some(editor)); + } + + Ok(None) + } +} diff --git a/src/editor/zellij.rs b/src/editor/zellij.rs new file mode 100644 index 00000000..b628a682 --- /dev/null +++ b/src/editor/zellij.rs @@ -0,0 +1,55 @@ +use std::process::Command; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +use crate::editor::run_cmd; + +#[derive(Deserialize)] +struct Pane { + id: u32, +} + +pub fn parse_pane_id(b: &[u8]) -> Option<(String, u32)> { + // Remove newline + let b = b.get("terminal_".len()..b.len().saturating_sub(1))?; + let id_str = str::from_utf8(b).ok()?; + + let (first, rest) = b.split_first()?; + let mut id = u32::from(first - b'0'); + + for c in rest { + id = 10 * id + u32::from(c - b'0'); + } + + Some((id_str.to_owned(), id)) +} + +pub fn pane_open(pane_id: u32) -> Result { + let mut stdout = run_cmd( + Command::new("zellij") + .arg("action") + .arg("list-panes") + .arg("-j"), + )?; + + // Remove newline + stdout.pop(); + + let panes = serde_json::de::from_slice::>(&stdout) + .context("Failed to parse the output of `zellij action list-panes -j`")?; + + Ok(panes.iter().any(|pane| pane.id == pane_id)) +} + +pub fn close_pane(pane_id: &str) -> Result<()> { + run_cmd( + Command::new("zellij") + .arg("action") + .arg("close-pane") + .arg("-p") + .arg(pane_id), + )?; + + Ok(()) +} diff --git a/src/embedded.rs b/src/embedded.rs index 51a14b6a..bee4119c 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -20,10 +20,10 @@ struct ExerciseFiles { } fn create_dir_if_not_exists(path: &str) -> Result<()> { - if let Err(e) = create_dir(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(Error::from(e).context(format!("Failed to create the directory {path}"))); - } + 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(()) @@ -85,7 +85,7 @@ impl EmbeddedFiles { exercise_path.truncate(prefix.len()); exercise_path.push_str(dir.name); exercise_path.push('/'); - exercise_path.push_str(&exercise_info.name); + exercise_path.push_str(exercise_info.name); exercise_path.push_str(".rs"); fs::write(&exercise_path, exercise_files.exercise) @@ -141,18 +141,19 @@ mod tests { use super::*; #[derive(Deserialize)] - struct ExerciseInfo { - dir: String, + struct ExerciseInfo<'a> { + dir: &'a str, } #[derive(Deserialize)] - struct InfoFile { - exercises: Vec, + struct InfoFile<'a> { + #[serde(borrow)] + exercises: Vec>, } #[test] fn dirs() { - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_file) + let exercises = toml::de::from_str::(EMBEDDED_FILES.info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/exercise.rs b/src/exercise.rs index 84908284..b969c69a 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,28 +1,34 @@ use anyhow::Result; use crossterm::{ - style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, QueueableCommand, + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, }; use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, - term::{self, terminal_file_link, write_ansi, CountedWrite}, + term::{self, CountedWrite, file_path, terminal_file_link, write_ansi}, }; /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; -pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> { +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: ")?; - if let Some(canonical_path) = term::canonicalize(solution_path) { - terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?; - } else { - stdout.write_all(solution_path.as_bytes())?; - } + 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") } @@ -42,17 +48,17 @@ fn run_bin( let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; - if let Some(output) = output { - if !success { - // This output is important to show the user that something went wrong. - // Otherwise, calling something like `exit(1)` in an exercise without further output - // leaves the user confused about why the exercise isn't done yet. - write_ansi(output, SetAttribute(Attribute::Bold)); - write_ansi(output, SetForegroundColor(Color::Red)); - output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); - write_ansi(output, ResetColor); - output.push(b'\n'); - } + if let Some(output) = output + && !success + { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. + write_ansi(output, SetAttribute(Attribute::Bold)); + write_ansi(output, SetForegroundColor(Color::Red)); + output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); + write_ansi(output, ResetColor); + output.push(b'\n'); } Ok(success) @@ -60,8 +66,8 @@ fn run_bin( /// See `info_file::ExerciseInfo` pub struct Exercise { - pub dir: Option<&'static str>, pub name: &'static str, + pub dir: Option<&'static str>, /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, pub canonical_path: Option, @@ -72,12 +78,18 @@ pub struct Exercise { } impl Exercise { - pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> { - if let Some(canonical_path) = self.canonical_path.as_deref() { - return terminal_file_link(writer, self.path, canonical_path, Color::Blue); - } - - writer.write_str(self.path) + pub fn terminal_file_link<'a>( + &self, + writer: &mut impl CountedWrite<'a>, + emit_file_links: bool, + ) -> io::Result<()> { + file_path(writer, Color::Blue, |writer| { + if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { + terminal_file_link(writer, self.path, canonical_path) + } else { + writer.write_str(self.path) + } + }) } } @@ -146,7 +158,6 @@ pub trait RunnableExercise { /// Compile, check and run the exercise. /// The output is written to the `output` buffer after clearing it. - #[inline] fn run_exercise(&self, output: Option<&mut Vec>, cmd_runner: &CmdRunner) -> Result { self.run::(self.name(), output, cmd_runner) } @@ -189,22 +200,18 @@ pub trait RunnableExercise { } impl RunnableExercise for Exercise { - #[inline] fn name(&self) -> &str { self.name } - #[inline] fn dir(&self) -> Option<&str> { self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } diff --git a/src/info_file.rs b/src/info_file.rs index fdc8f0f3..26bb1a2c 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Error, Result}; +use anyhow::{Context, Error, Result, bail}; use serde::Deserialize; use std::{fs, io::ErrorKind}; @@ -8,9 +8,9 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; #[derive(Deserialize)] pub struct ExerciseInfo { /// Exercise's unique name. - pub name: String, + pub name: &'static str, /// Exercise's directory name inside the `exercises/` directory. - pub dir: Option, + pub dir: Option<&'static str>, /// Run `cargo test` on the exercise. #[serde(default = "default_true")] pub test: bool, @@ -18,12 +18,11 @@ pub struct ExerciseInfo { #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. - pub hint: String, + pub hint: &'static str, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. #[serde(default)] pub skip_check_unsolved: bool, } -#[inline(always)] const fn default_true() -> bool { true } @@ -31,7 +30,7 @@ const fn default_true() -> bool { impl ExerciseInfo { /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { - let mut path = if let Some(dir) = &self.dir { + let mut path = if let Some(dir) = self.dir { // 14 = 10 + 1 + 3 // exercises/ + / + .rs let mut path = String::with_capacity(14 + dir.len() + self.name.len()); @@ -47,7 +46,7 @@ impl ExerciseInfo { path }; - path.push_str(&self.name); + path.push_str(self.name); path.push_str(".rs"); path @@ -55,22 +54,18 @@ impl ExerciseInfo { } impl RunnableExercise for ExerciseInfo { - #[inline] fn name(&self) -> &str { - &self.name + self.name } - #[inline] fn dir(&self) -> Option<&str> { - self.dir.as_deref() + self.dir } - #[inline] fn strict_clippy(&self) -> bool { self.strict_clippy } - #[inline] fn test(&self) -> bool { self.test } @@ -79,27 +74,35 @@ impl RunnableExercise for ExerciseInfo { /// The deserialized `info.toml` file. #[derive(Deserialize)] pub struct InfoFile { - /// For possible breaking changes in the future for third-party exercises. + /// For possible breaking changes in the future for community exercises. pub format_version: u8, /// Shown to users when starting with the exercises. - pub welcome_message: Option, + pub welcome_message: Option<&'static str>, /// Shown to users after finishing all exercises. - pub final_message: Option, + pub final_message: Option<&'static str>, /// List of all exercises. pub exercises: Vec, } impl InfoFile { /// Official exercises: Parse the embedded `info.toml` file. - /// Third-party exercises: Parse the `info.toml` file in the current directory. + /// Community exercises: Parse the `info.toml` file in the current directory. pub fn parse() -> Result { // Read a local `info.toml` if it exists. - let slf = match fs::read_to_string("info.toml") { - Ok(file_content) => toml_edit::de::from_str::(&file_content) - .context("Failed to parse the `info.toml` file")?, + let slf = match fs::read("info.toml") { + Ok(file_content) => { + // Remove `\r` on Windows. + // Leaking is fine since the info file is used until the end of the program. + let file_content = + String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect()) + .context("Failed to parse `info.toml` as UTF8")? + .leak(); + toml::de::from_str::(file_content) + .context("Failed to parse the `info.toml` file")? + } Err(e) => { if e.kind() == ErrorKind::NotFound { - return toml_edit::de::from_str(EMBEDDED_FILES.info_file) + return toml::de::from_str(EMBEDDED_FILES.info_file) .context("Failed to parse the embedded `info.toml` file"); } diff --git a/src/init.rs b/src/init.rs index ce49bb65..f043bd48 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,14 +1,14 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use crossterm::{ - style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, QueueableCommand, + style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, }; use serde::Deserialize; use std::{ - env::set_current_dir, + env::{current_dir, set_current_dir}, fs::{self, create_dir}, io::{self, Write}, - path::{Path, PathBuf}, + path::Path, process::{Command, Stdio}, }; @@ -18,8 +18,9 @@ use crate::{ }; #[derive(Deserialize)] -struct CargoLocateProject { - root: PathBuf, +struct CargoLocateProject<'a> { + #[serde(borrow)] + root: &'a Path, } pub fn init() -> Result<()> { @@ -35,7 +36,27 @@ pub fn init() -> Result<()> { .stdin(Stdio::null()) .stderr(Stdio::null()) .output() - .context(CARGO_LOCATE_PROJECT_ERR)?; + .context( + "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 init_git = true; @@ -52,15 +73,19 @@ pub fn init() -> Result<()> { )? .root; - let workspace_manifest_content = fs::read_to_string(&workspace_manifest) + let workspace_manifest_content = fs::read_to_string(workspace_manifest) .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?; - if !workspace_manifest_content.contains("[workspace]\n") + if !workspace_manifest_content.contains("[workspace]") && !workspace_manifest_content.contains("workspace.") { - bail!("The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory"); + bail!( + "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.\nPress ENTER to continue ")?; + stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\n\ + Press ENTER to continue ")?; press_enter_prompt(&mut stdout)?; // Make sure "rustlings" is added to `workspace.members` by making @@ -75,15 +100,19 @@ pub fn init() -> Result<()> { .stdout(Stdio::null()) .status()?; if !status.success() { - bail!("Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory"); + bail!( + "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")?; - fs::remove_dir_all("rustlings") + fs::remove_dir_all(rustlings_dir) .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; } else { - stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\n\ + Press ENTER to continue ")?; press_enter_prompt(&mut stdout)?; } @@ -140,14 +169,27 @@ pub fn init() -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - if init_git { - // Ignore any Git error because Git initialization is not required. - let _ = Command::new("git") - .arg("init") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + if init_git && let Ok(dir) = current_dir() { + let mut dir = dir.as_path(); + + loop { + if dir.join(".git").exists() || dir.join(".jj").exists() { + break; + } + + if let Some(parent) = dir.parent() { + dir = parent; + } else { + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + break; + } + } } stdout.queue(SetForegroundColor(Color::Green))?; @@ -162,10 +204,6 @@ pub fn init() -> Result<()> { 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() { // DON'T EDIT THIS SOLUTION FILE! // It will be automatically filled after you finish the exercise. @@ -174,6 +212,7 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() { pub const RUST_ANALYZER_TOML: &[u8] = br#"check.command = "clippy" check.extraArgs = ["--profile", "test"] +cargo.targetDir = true "#; const GITIGNORE: &[u8] = b"Cargo.lock diff --git a/src/list.rs b/src/list.rs index 9f243a17..db466c2f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,20 +1,20 @@ use anyhow::{Context, Result}; use crossterm::{ - cursor, + QueueableCommand, cursor, event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind, }, terminal::{ - disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, - LeaveAlternateScreen, + DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen, + disable_raw_mode, enable_raw_mode, }, - QueueableCommand, }; use std::io::{self, StdoutLock, Write}; -use crate::app_state::AppState; - -use self::state::{Filter, ListState}; +use crate::{ + app_state::AppState, + list::state::{Filter, ListState}, +}; mod scroll_state; mod state; @@ -83,7 +83,7 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> } } KeyCode::Char('r') => list_state.reset_selected()?, - KeyCode::Char('c') => { + KeyCode::Char('c') | KeyCode::Enter => { if list_state.selected_to_current_exercise()? { return Ok(()); } diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 2c02ed4f..299db568 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -19,7 +19,6 @@ impl ScrollState { } } - #[inline] pub fn offset(&self) -> usize { self.offset } @@ -41,7 +40,6 @@ impl ScrollState { .min(global_max_offset); } - #[inline] pub fn selected(&self) -> Option { self.selected } @@ -86,12 +84,10 @@ impl ScrollState { self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1))); } - #[inline] fn update_scroll_padding(&mut self) { self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding); } - #[inline] pub fn max_n_rows_to_display(&self) -> usize { self.max_n_rows_to_display } diff --git a/src/list/state.rs b/src/list/state.rs index 0670fa46..8b0c4df8 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,11 +1,11 @@ use anyhow::{Context, Result}; use crossterm::{ + QueueableCommand, cursor::{MoveTo, MoveToNextLine}, style::{ Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, }, terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate}, - QueueableCommand, }; use std::{ fmt::Write as _, @@ -15,11 +15,10 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, - term::{progress_bar, CountedWrite, MaxLenWriter}, + list::scroll_state::ScrollState, + term::{CountedWrite, MaxLenWriter, progress_bar}, }; -use super::scroll_state::ScrollState; - const COL_SPACING: usize = 2; const SELECTED_ROW_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Reverse) @@ -118,8 +117,8 @@ impl<'a> ListState<'a> { } fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> { - if !self.search_query.is_empty() { - if let Some((pre_highlight, highlight, post_highlight)) = exercise + if !self.search_query.is_empty() + && let Some((pre_highlight, highlight, post_highlight)) = exercise .name .find(&self.search_query) .and_then(|ind| exercise.name.split_at_checked(ind)) @@ -127,13 +126,12 @@ impl<'a> ListState<'a> { 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(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) @@ -186,13 +184,7 @@ impl<'a> ListState<'a> { writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; - // The list links aren't shown correctly in VS Code on Windows. - // But VS Code shows its own links anyway. - if self.app_state.vs_code() { - writer.write_str(exercise.path)?; - } else { - exercise.terminal_file_link(&mut writer)?; - } + exercise.terminal_file_link(&mut writer, self.app_state.emit_file_links())?; writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?; @@ -236,7 +228,7 @@ impl<'a> ListState<'a> { progress_bar( &mut MaxLenWriter::new(stdout, self.term_width as usize), self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; next_ln(stdout)?; @@ -245,11 +237,24 @@ impl<'a> ListState<'a> { if self.message.is_empty() { // Help footer message if self.scroll_state.selected().is_some() { - writer.write_str("↓/j ↑/k home/g end/G | ontinue at | eset exercise")?; + writer.write_str("↓/")?; + hotkey(&mut writer, b"j")?; + writer.write_str(" ↑/")?; + hotkey(&mut writer, b"k")?; + writer.write_ascii(b" home/")?; + hotkey(&mut writer, b"g")?; + writer.write_ascii(b" end/")?; + hotkey(&mut writer, b"G")?; + writer.write_str(" | ↩️/")?; + hotkey(&mut writer, b"c")?; + writer.write_ascii(b"ontinue at | ")?; + hotkey(&mut writer, b"r")?; + writer.write_ascii(b"eset exercise")?; next_ln(stdout)?; writer = MaxLenWriter::new(stdout, self.term_width as usize); - writer.write_ascii(b"earch | filter ")?; + hotkey(&mut writer, b"s")?; + writer.write_ascii(b"earch | filter ")?; } else { // Nothing selected (and nothing shown), so only display filter and quit. writer.write_ascii(b"filter ")?; @@ -257,27 +262,41 @@ impl<'a> ListState<'a> { match self.filter { Filter::Done => { + writer.stdout.queue(SetAttribute(Attribute::Underlined))?; + hotkey(&mut writer, b"d")?; writer .stdout .queue(SetForegroundColor(Color::Magenta))? .queue(SetAttribute(Attribute::Underlined))?; - writer.write_ascii(b"one")?; + writer.write_str("one")?; writer.stdout.queue(ResetColor)?; - writer.write_ascii(b"/

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

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

ending")?, + Filter::None => { + hotkey(&mut writer, b"d")?; + writer.write_ascii(b"one/")?; + hotkey(&mut writer, b"p")?; + writer.write_ascii(b"ending")?; + } } - writer.write_ascii(b" | uit list")?; + writer.write_ascii(b" | ")?; + hotkey(&mut writer, b"q")?; + writer.write_ascii(b"uit list")?; } else { writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.write_str(&self.message)?; @@ -311,7 +330,6 @@ impl<'a> ListState<'a> { self.scroll_state.set_n_rows(n_rows); } - #[inline] pub fn filter(&self) -> Filter { self.filter } @@ -321,22 +339,18 @@ impl<'a> ListState<'a> { self.update_rows(); } - #[inline] pub fn select_next(&mut self) { self.scroll_state.select_next(); } - #[inline] pub fn select_previous(&mut self) { self.scroll_state.select_previous(); } - #[inline] pub fn select_first(&mut self) { self.scroll_state.select_first(); } - #[inline] pub fn select_last(&mut self) { self.scroll_state.select_last(); } @@ -373,11 +387,11 @@ impl<'a> ListState<'a> { let exercise_ind = self.selected_to_exercise_ind(selected)?; let exercise_name = self.app_state.reset_exercise_by_ind(exercise_ind)?; - self.update_rows(); write!( self.message, "The exercise `{exercise_name}` has been reset", )?; + self.update_rows(); Ok(()) } @@ -422,3 +436,14 @@ impl<'a> ListState<'a> { Ok(true) } } + +/// Draw an emphasized hotkey in the list footer. +fn hotkey(writer: &mut MaxLenWriter, hotkey: &[u8]) -> io::Result<()> { + writer + .stdout + .queue(SetForegroundColor(Color::Yellow))? + .queue(SetAttribute(Attribute::Bold))?; + writer.write_ascii(hotkey)?; + writer.stdout.queue(ResetColor)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a46f9fa1..d97a79f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,27 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use app_state::StateFileStatus; -use clap::{Parser, Subcommand}; +use clap::Parser; use std::{ + env, io::{self, IsTerminal, Write}, path::Path, process::ExitCode, }; use term::{clear_terminal, press_enter_prompt}; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile}; +use crate::{ + app_state::AppState, + cli::{Args, Command}, + editor::Editor, + info_file::InfoFile, +}; mod app_state; mod cargo_toml; +mod cli; mod cmd; mod dev; +mod editor; mod embedded; mod exercise; mod info_file; @@ -26,48 +34,6 @@ mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code -#[derive(Parser)] -#[command(version)] -struct Args { - #[command(subcommand)] - command: Option, - /// Manually run the current exercise using `r` in the watch mode. - /// Only use this if Rustlings fails to detect exercise file changes. - #[arg(long)] - manual_run: bool, - - /// Change rustbook url to passed one. - #[arg(long)] - base_url: Option, -} - -#[derive(Subcommand)] -enum Subcommands { - /// Initialize the official Rustlings exercises - Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified - Run { - /// The name of the exercise - name: Option, - }, - /// Check all the exercises, marking them as done or pending accordingly. - CheckAll, - /// Reset a single exercise - Reset { - /// The name of the exercise - name: String, - }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified - Hint { - /// The name of the exercise - name: Option, - }, - /// Commands for developing (third-party) Rustlings exercises - #[command(subcommand)] - Dev(DevCommands), -} - fn main() -> Result { let args = Args::parse(); @@ -77,8 +43,8 @@ fn main() -> Result { 'priority_cmd: { match args.command { - Some(Subcommands::Init) => init::init().context("Initialization failed")?, - Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + Some(Command::Init) => init::init().context("Initialization failed")?, + Some(Command::Dev(dev_command)) => dev_command.run()?, _ => break 'priority_cmd, } @@ -96,10 +62,19 @@ fn main() -> Result { bail!(FORMAT_VERSION_HIGHER_ERR); } + let vs_code_term = env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"); + let editor = if args.no_editor { + None + } else { + Editor::new(args.edit_cmd, vs_code_term)? + }; + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), args.base_url, + editor, + vs_code_term, )?; // Show the welcome message if the state file doesn't exist yet. @@ -110,7 +85,11 @@ fn main() -> Result { clear_terminal(&mut stdout)?; let welcome_message = welcome_message.trim_ascii(); - write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + write!( + stdout, + "{welcome_message}\n\n\ + Press ENTER to continue " + )?; press_enter_prompt(&mut stdout)?; clear_terminal(&mut stdout)?; // Flush to be able to show errors occurring before printing a newline to stdout. @@ -130,7 +109,7 @@ fn main() -> Result { None } else { // For the notify event handler thread. - // Leaking is not a problem because the slice lives until the end of the program. + // Leaking is fine since the slice is used until the end of the program. Some( &*app_state .exercises() @@ -142,14 +121,15 @@ fn main() -> Result { }; watch::watch(&mut app_state, notify_exercise_names)?; + app_state.close_editor()?; } - Some(Subcommands::Run { name }) => { + Some(Command::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } return run::run(&mut app_state); } - Some(Subcommands::CheckAll) => { + Some(Command::CheckAll) => { let mut stdout = io::stdout().lock(); if let Some(first_pending_exercise_ind) = app_state.check_all_exercises(&mut stdout)? { if app_state.current_exercise().done { @@ -169,27 +149,40 @@ fn main() -> Result { } app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .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)?; } + + app_state.render_final_message(&mut stdout)?; } - Some(Subcommands::Reset { name }) => { + Some(Command::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - let exercise_path = app_state.reset_current_exercise()?; - println!("The exercise {exercise_path} has been reset"); + app_state.reset_current_exercise()?; + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"The exercise ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + stdout.write_all(b" has been reset\n")?; } - Some(Subcommands::Hint { name }) => { + Some(Command::Hint { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - println!("{}", app_state.current_exercise().hint); + + let current_exercise = app_state.current_exercise(); + let mut stdout = io::stdout().lock(); + stdout.write_all(b"Current exercise: ")?; + current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?; + + stdout.write_all(b"\n\nHint:\n")?; + stdout.write_all(current_exercise.hint.as_bytes())?; + stdout.write_all(b"\n")?; } // Handled in an earlier match. - Some(Subcommands::Init | Subcommands::Dev(_)) => (), + Some(Command::Init | Command::Dev(_)) => (), } Ok(ExitCode::SUCCESS) diff --git a/src/run.rs b/src/run.rs index ac8b26ad..b473fc20 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crossterm::{ - style::{Color, ResetColor, SetForegroundColor}, QueueableCommand, + style::{Color, ResetColor, SetForegroundColor}, }; use std::{ io::{self, Write}, @@ -10,7 +10,7 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, - exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, + exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, }; pub fn run(app_state: &mut AppState) -> Result { @@ -27,7 +27,7 @@ pub fn run(app_state: &mut AppState) -> Result { stdout.write_all(b"Ran ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b" with errors\n")?; return Ok(ExitCode::FAILURE); @@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result { if let Some(solution_path) = app_state.current_solution_path()? { stdout.write_all(b"\n")?; - solution_link_line(&mut stdout, &solution_path)?; + solution_link_line(&mut stdout, &solution_path, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } @@ -50,7 +50,7 @@ pub fn run(app_state: &mut AppState) -> Result { stdout.write_all(b"Next exercise: ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } ExercisesProgress::AllDone => (), diff --git a/src/term.rs b/src/term.rs index cb0a07ce..2467b450 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,8 +1,8 @@ use crossterm::{ + Command, QueueableCommand, cursor::MoveTo, style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, - Command, QueueableCommand, }; use std::{ fmt, fs, @@ -18,7 +18,6 @@ pub struct MaxLenWriter<'a, 'lock> { } impl<'a, 'lock> MaxLenWriter<'a, 'lock> { - #[inline] pub fn new(stdout: &'a mut StdoutLock<'lock>, max_len: usize) -> Self { Self { stdout, @@ -28,7 +27,6 @@ impl<'a, 'lock> MaxLenWriter<'a, 'lock> { } // Additional is for emojis that take more space. - #[inline] pub fn add_to_len(&mut self, additional: usize) { self.len += additional; } @@ -64,24 +62,20 @@ impl<'lock> CountedWrite<'lock> for MaxLenWriter<'_, 'lock> { Ok(()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'lock> { self.stdout } } impl<'a> CountedWrite<'a> for StdoutLock<'a> { - #[inline] fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> { self.write_all(ascii) } - #[inline] fn write_str(&mut self, unicode: &str) -> io::Result<()> { self.write_all(unicode.as_bytes()) } - #[inline] fn stdout(&mut self) -> &mut StdoutLock<'a> { self } @@ -160,21 +154,52 @@ impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> { } } +pub struct ProgressCounter<'a, 'lock> { + stdout: &'a mut StdoutLock<'lock>, + total: usize, + counter: usize, +} + +impl<'a, 'lock> ProgressCounter<'a, 'lock> { + pub fn new(stdout: &'a mut StdoutLock<'lock>, total: usize) -> io::Result { + write!(stdout, "Progress: 0/{total}")?; + stdout.flush()?; + + Ok(Self { + stdout, + total, + counter: 0, + }) + } + + pub fn increment(&mut self) -> io::Result<()> { + self.counter += 1; + write!(self.stdout, "\rProgress: {}/{}", self.counter, self.total)?; + self.stdout.flush() + } +} + +impl Drop for ProgressCounter<'_, '_> { + fn drop(&mut self) { + let _ = self.stdout.write_all(b"\n\n"); + } +} + pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, - progress: u16, - total: u16, + progress: u32, + total: u32, term_width: u16, ) -> io::Result<()> { - debug_assert!(total <= 999); - debug_assert!(progress <= total); - const PREFIX: &[u8] = b"Progress: ["; const PREFIX_WIDTH: u16 = PREFIX.len() as u16; const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16; const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + debug_assert!(total <= 999); + debug_assert!(progress <= total); + if term_width < MIN_LINE_WIDTH { writer.write_ascii(b"Progress: ")?; // Integers are in ASCII. @@ -184,7 +209,8 @@ pub fn progress_bar<'a>( let stdout = writer.stdout(); stdout.write_all(PREFIX)?; - let width = term_width - WRAPPER_WIDTH; + // Use u32 to prevent the intermediate multiplication from overflowing + let width = u32::from(term_width - WRAPPER_WIDTH); let filled = (width * progress) / total; stdout.queue(SetForegroundColor(Color::Green))?; @@ -194,14 +220,13 @@ pub fn progress_bar<'a>( if filled < width { stdout.write_all(b">")?; - } - let width_minus_filled = width - filled; - if width_minus_filled > 1 { - let red_part_width = width_minus_filled - 1; - stdout.queue(SetForegroundColor(Color::Red))?; - for _ in 0..red_part_width { - stdout.write_all(b"-")?; + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + stdout.queue(SetForegroundColor(Color::Red))?; + for _ in 1..width_minus_filled { + stdout.write_all(b"-")?; + } } } @@ -241,22 +266,18 @@ pub fn canonicalize(path: &str) -> Option { }) } -pub fn terminal_file_link<'a>( - writer: &mut impl CountedWrite<'a>, - path: &str, - canonical_path: &str, +pub fn file_path<'a, W: CountedWrite<'a>>( + writer: &mut W, color: Color, + f: impl FnOnce(&mut W) -> io::Result<()>, ) -> io::Result<()> { writer .stdout() .queue(SetForegroundColor(color))? .queue(SetAttribute(Attribute::Underlined))?; - writer.stdout().write_all(b"\x1b]8;;file://")?; - writer.stdout().write_all(canonical_path.as_bytes())?; - writer.stdout().write_all(b"\x1b\\")?; - // Only this part is visible. - writer.write_str(path)?; - writer.stdout().write_all(b"\x1b]8;;\x1b\\")?; + + f(writer)?; + writer .stdout() .queue(SetForegroundColor(Color::Reset))? @@ -265,6 +286,19 @@ pub fn terminal_file_link<'a>( Ok(()) } +pub fn terminal_file_link<'a>( + writer: &mut impl CountedWrite<'a>, + path: &str, + canonical_path: &str, +) -> io::Result<()> { + writer.stdout().write_all(b"\x1b]8;;file://")?; + writer.stdout().write_all(canonical_path.as_bytes())?; + writer.stdout().write_all(b"\x1b\\")?; + // Only this part is visible. + writer.write_str(path)?; + writer.stdout().write_all(b"\x1b]8;;\x1b\\") +} + pub fn write_ansi(output: &mut Vec, command: impl Command) { struct FmtWriter<'a>(&'a mut Vec); diff --git a/src/watch.rs b/src/watch.rs index 6259c9df..f3804a40 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -13,10 +13,9 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, list, + watch::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}, }; -use self::{notify_event::NotifyEventHandler, state::WatchState, terminal_event::InputEvent}; - mod notify_event; mod state; mod terminal_event; @@ -28,7 +27,6 @@ static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); pub struct InputPauseGuard(()); impl InputPauseGuard { - #[inline] pub fn scoped_pause() -> Self { EXERCISE_RUNNING.store(true, Relaxed); Self(()) @@ -36,7 +34,6 @@ impl InputPauseGuard { } impl Drop for InputPauseGuard { - #[inline] fn drop(&mut self) { EXERCISE_RUNNING.store(false, Relaxed); } @@ -74,7 +71,9 @@ fn run_watch( let mut watcher = RecommendedWatcher::new( notify_event_handler, - Config::default().with_poll_interval(Duration::from_secs(1)), + Config::default() + .with_follow_symlinks(false) + .with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; @@ -151,6 +150,7 @@ pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result<()> { + // TODO: Use cfg_select! after bumping MSRV to at least 1.95 #[cfg(not(windows))] { let stdin_fd = rustix::stdio::stdin(); @@ -173,8 +173,7 @@ pub fn watch( watch_list_loop(app_state, notify_exercise_names) } -const QUIT_MSG: &[u8] = b" - +const QUIT_MSG: &[u8] = b"q\n We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again in this directory. "; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 2051e544..edd9c720 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,18 +1,18 @@ use anyhow::{Context, Result}; use notify::{ - event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}, Event, EventKind, + event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}, }; use std::{ sync::{ atomic::Ordering::Relaxed, - mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender}, + mpsc::{RecvTimeoutError, Sender, SyncSender, sync_channel}, }, thread, time::Duration, }; -use super::{WatchEvent, EXERCISE_RUNNING}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); diff --git a/src/watch/state.rs b/src/watch/state.rs index 5263bc57..8bbdc585 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,25 +1,25 @@ use anyhow::{Context, Result}; use crossterm::{ + QueueableCommand, style::{ Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, }, - terminal, QueueableCommand, + terminal, }; use std::{ io::{self, Read, StdoutLock, Write}, - sync::mpsc::{sync_channel, Sender, SyncSender}, + sync::mpsc::{Sender, SyncSender, sync_channel}, thread, }; use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, - exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, + exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, term::progress_bar, + watch::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}, }; -use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent}; - const HEADING_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Bold) .with(Attribute::Underlined); @@ -59,7 +59,7 @@ impl<'a> WatchState<'a> { watch_event_sender, terminal_event_unpause_receiver, manual_run, - ) + ); }) .context("Failed to spawn a thread to handle terminal events")?; @@ -78,14 +78,16 @@ impl<'a> WatchState<'a> { // Ignore any input until running the exercise is done. let _input_pause_guard = InputPauseGuard::scoped_pause(); - self.show_hint = false; - writeln!( stdout, "\nChecking the exercise `{}`. Please wait…", self.app_state.current_exercise().name, )?; + let editor_handle = self.app_state.open_editor()?; + + self.show_hint = false; + let success = self .app_state .current_exercise() @@ -105,7 +107,9 @@ impl<'a> WatchState<'a> { self.done_status = DoneStatus::Pending; } + self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; + Ok(()) } @@ -127,9 +131,10 @@ impl<'a> WatchState<'a> { match answer[0] { b'y' | b'Y' => { + self.app_state.close_editor()?; self.app_state.reset_current_exercise()?; - // The file watcher reruns the exercise otherwise. + // The file watcher reruns the exercise otherwise if self.manual_run { self.run_current_exercise(stdout)?; } @@ -232,7 +237,7 @@ impl<'a> WatchState<'a> { stdout.write_all(b"\n")?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - solution_link_line(stdout, solution_path)?; + solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?; } stdout.write_all( @@ -244,14 +249,14 @@ impl<'a> WatchState<'a> { progress_bar( stdout, self.app_state.n_done(), - self.app_state.exercises().len() as u16, + self.app_state.exercises().len() as u32, self.term_width, )?; stdout.write_all(b"\nCurrent exercise: ")?; self.app_state .current_exercise() - .terminal_file_link(stdout)?; + .terminal_file_link(stdout, self.app_state.emit_file_links())?; stdout.write_all(b"\n\n")?; self.show_prompt(stdout)?; diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 48411db0..4f0685b6 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,7 +4,7 @@ use std::sync::{ mpsc::{Receiver, Sender}, }; -use super::{WatchEvent, EXERCISE_RUNNING}; +use crate::watch::{EXERCISE_RUNNING, WatchEvent}; pub enum InputEvent { Next, @@ -47,7 +47,7 @@ pub fn terminal_event_handler( // Pause input until quitting the confirmation prompt. if unpause_receiver.recv().is_err() { return; - }; + } continue; } @@ -64,7 +64,7 @@ pub fn terminal_event_handler( return; } } - Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => continue, + Ok(Event::FocusGained | Event::FocusLost | Event::Mouse(_)) => (), Err(e) => break WatchEvent::TerminalEventErr(e), } }; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bb3a084b..d38d4e93 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,11 +1,9 @@ use std::{ - env::{self, consts::EXE_SUFFIX}, process::{Command, Stdio}, str::from_utf8, }; enum Output<'a> { - FullStdout(&'a str), PartialStdout(&'a str), PartialStderr(&'a str), } @@ -20,39 +18,24 @@ struct Cmd<'a> { } impl<'a> Cmd<'a> { - #[inline] fn current_dir(&mut self, current_dir: &'a str) -> &mut Self { self.current_dir = Some(current_dir); self } - #[inline] fn args(&mut self, args: &'a [&'a str]) -> &mut Self { self.args = args; self } - #[inline] fn output(&mut self, output: Output<'a>) -> &mut Self { self.output = Some(output); self } + #[track_caller] fn assert(&self, success: bool) { - let rustlings_bin = { - let mut path = env::current_exe().unwrap(); - // Pop test binary name - path.pop(); - // Pop `/deps` - path.pop(); - - path.push("rustlings"); - let mut path = path.into_os_string(); - path.push(EXE_SUFFIX); - path - }; - - let mut cmd = Command::new(rustlings_bin); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_rustlings")); if let Some(current_dir) = self.current_dir { cmd.current_dir(current_dir); @@ -60,38 +43,32 @@ impl<'a> Cmd<'a> { cmd.args(self.args).stdin(Stdio::null()); - let status = match self.output { - None => cmd - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .unwrap(), - Some(FullStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); - assert_eq!(from_utf8(&output.stdout).unwrap(), stdout); - output.status - } + let output = cmd.output().unwrap(); + match self.output { + None => (), Some(PartialStdout(stdout)) => { - let output = cmd.stderr(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stdout).unwrap().contains(stdout)); - output.status } Some(PartialStderr(stderr)) => { - let output = cmd.stdout(Stdio::null()).output().unwrap(); assert!(from_utf8(&output.stderr).unwrap().contains(stderr)); - output.status } }; - assert_eq!(status.success(), success, "{cmd:?}"); + if output.status.success() != success { + panic!( + "{cmd:?}\n\nstdout:\n{}\n\nstderr:\n{}", + from_utf8(&output.stdout).unwrap(), + from_utf8(&output.stderr).unwrap(), + ); + } } - #[inline] + #[track_caller] fn success(&self) { self.assert(true); } - #[inline] + #[track_caller] fn fail(&self) { self.assert(false); } @@ -148,7 +125,7 @@ fn hint() { Cmd::default() .current_dir("tests/test_exercises") .args(&["hint", "test_failure"]) - .output(FullStdout("The answer to everything: 42\n")) + .output(PartialStdout("\n\nHint:\nThe answer to everything: 42\n")) .success(); } diff --git a/tests/test_exercises/dev/Cargo.toml b/tests/test_exercises/dev/Cargo.toml index 01fe7c10..74dcc20a 100644 --- a/tests/test_exercises/dev/Cargo.toml +++ b/tests/test_exercises/dev/Cargo.toml @@ -7,5 +7,5 @@ bin = [ [package] name = "test_exercises" -edition = "2021" +edition = "2024" publish = false diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..648e0774 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,7 @@ +/node_modules/ +/package-lock.json + +/public/ + +/static/main.css +/static/processed_images/ diff --git a/website/config.toml b/website/config.toml new file mode 100644 index 00000000..73ae7f2c --- /dev/null +++ b/website/config.toml @@ -0,0 +1,41 @@ +base_url = "https://rustlings.rust-lang.org" +title = "Rustlings" +description = "Small exercises to get you used to reading and writing Rust code!" + +compile_sass = false +build_search_index = false + +[markdown] +insert_anchor_links = "heading" + +[markdown.highlighting] +theme = "dracula" + +[extra] +logo_path = "images/happy_ferris.svg" + +[[extra.menu_items]] +name = "Rustlings" +url = "@/_index.md" +[[extra.menu_items]] +name = "Setup" +url = "@/setup/index.md" +[[extra.menu_items]] +name = "Usage" +url = "@/usage/index.md" +[[extra.menu_items]] +name = "Community Exercises" +url = "@/community-exercises/index.md" +[[extra.menu_items]] +name = "Q&A" +url = "https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=" + +[[extra.footer_items]] +name = "Repository" +url = "https://github.com/rust-lang/rustlings" +[[extra.footer_items]] +name = "Changelog" +url = "https://github.com/rust-lang/rustlings/blob/main/CHANGELOG.md" +[[extra.footer_items]] +name = "MIT License" +url = "https://github.com/rust-lang/rustlings/blob/main/LICENSE" diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 00000000..4bb4483b --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,21 @@ ++++ ++++ + +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) πŸ“šοΈ_ + + + +## Quick start + +```bash +# Installation +cargo install rustlings +# Initialization +rustlings init +# Moving into new directory +cd rustlings +# Starting Rustlings +rustlings +``` + +Visit the [**setup**](@/setup/index.md) page for more details 🧰 diff --git a/website/content/community-exercises/index.md b/website/content/community-exercises/index.md new file mode 100644 index 00000000..5d4d0d20 --- /dev/null +++ b/website/content/community-exercises/index.md @@ -0,0 +1,75 @@ ++++ +title = "Community Exercises" ++++ + +## List of Community Exercises + +- πŸ‡―πŸ‡΅ [Japanese Rustlings](https://github.com/sotanengel/rustlings-jp):A Japanese translation of the Rustlings exercises. +- πŸ‡¨πŸ‡³ [Simplified Chinese Rustlings](https://github.com/SandmeyerX/rustlings-zh-cn): A simplified Chinese translation of the Rustlings exercises. +- πŸ‡ΊπŸ‡¦ [Rustlings in Ukrainian](https://github.com/noroutine/rustlings-ua): Translation of the Rustlings exercises in Ukrainian. +- πŸ‡°πŸ‡· [Korean Rustlings](https://github.com/eoncheole/rustlings-kr): A Korean translation of the Rustlings exercises. + +> You can use the same `rustlings` program that you installed with `cargo install rustlings` to run community exercises. + +## Creating Community Exercises + +Rustling's support for community exercises allows you to create your own exercises to focus on some specific topic. +You could also offer a translation of the original Rustlings exercises as community exercises. + +### Getting Started + +To create community exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`. +This command will, similar to `cargo new PROJECT_NAME`, create the template directory `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. + +### Creating an Exercise + +Here is an example of the metadata of one exercise: + +```toml +[[exercises]] +name = "intro1" +hint = """ +To finish this exercise, you need to … +These links 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 πŸŽ‰ + +### Cargo.toml + +Except of the `bin` list, you can modify the `Cargo.toml` file as you want. + +> The `bin` list is automatically updated by running `rustlings dev update` + +- You can add dependencies in the `[dependencies]` table. +- You might want to [configure some lints](https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section) for all exercises. You can do so in the `[lints.rust]` and `[lints.clippy]` tables. + +### Publishing + +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 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 your users to not open the solution too early πŸ˜‰ + +### Sharing + +After publishing your community exercises, open an issue or a pull request in the [official Rustlings repository](https://github.com/rust-lang/rustlings) to add your project to the [list of community exercises](#list-of-community-exercises) πŸ˜ƒ diff --git a/website/content/setup/index.md b/website/content/setup/index.md new file mode 100644 index 00000000..7595b628 --- /dev/null +++ b/website/content/setup/index.md @@ -0,0 +1,82 @@ ++++ +title = "Setup" ++++ + + + +## Installing Rust + +Before installing Rustlings, you must have the **latest version of Rust** installed. +Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions. +This will also install _Cargo_, Rust's package/project manager. + +> 🐧 If you are on **Linux**, make sure you have `gcc` installed (_for a linker_). +> +> Debian: `sudo apt install gcc`\ +> Fedora: `sudo dnf install gcc` +> +> 🍎 If you are on **MacOS**, make sure you have _Xcode and its developer tools_ installed: `xcode-select --install` + +## Installing Rustlings + +The following command will download and compile Rustlings: + +```bash +cargo install rustlings +``` + +{% details(summary="If the installation fails…") %} + +- 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) + +{% end %} + +## Initialization + +After installing Rustlings, run the following command to initialize the `rustlings/` directory: + +```bash +rustlings init +``` + +{% details(summary="If the command rustlings can't be found…") %} + +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. + +- Either add `~/.cargo/bin` manually to `PATH` +- Or uninstall Rust from the package manager and [install it using the official way with `rustup`](https://www.rust-lang.org/tools/install) + +{% end %} + +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). + +### Offline documentation + +Whenever you're working on Rustlings offline, you can access a local copy of the book or the standard library documentation by running `rustup doc --book` or `rustup doc --std`. + +## Usage + +After being done with the setup, visit the [**usage**](@/usage/index.md) page for some info about using Rustlings πŸš€ diff --git a/website/content/usage/index.md b/website/content/usage/index.md new file mode 100644 index 00000000..88dabf4a --- /dev/null +++ b/website/content/usage/index.md @@ -0,0 +1,55 @@ ++++ +title = "Usage" ++++ + + + +## Doing exercises + +The exercises are sorted by topic and can be found in the subdirectory `exercises/`. +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 the [initialization](@/setup/index.md#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="If detecting file changes in the exercises/ directory fails…") %} + +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 a virtual machine (e.g. WSL). + +{% end %} + +## 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 the selected exercise (you need to _reload/reopen_ its file in your editor afterwards) + +See the footer of the list for all possible keys. + +## Questions? + +If you need any help while doing the exercises and the builtin hints aren't helpful, feel free to ask in the [_Q&A_ discussions](https://github.com/rust-lang/rustlings/discussions/categories/q-a?discussions_q=) if your question isn't answered there πŸ’‘ + +## 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. + +> If you want to create your own Rustlings exercises, visit the [**community exercises**](@/community-exercises/index.md) page πŸ—οΈ diff --git a/website/input.css b/website/input.css new file mode 100644 index 00000000..01c956d6 --- /dev/null +++ b/website/input.css @@ -0,0 +1,50 @@ +@import 'tailwindcss'; + +@layer base { + h1 { + @apply text-4xl mt-3 mb-3 font-bold; + } + h2 { + @apply text-3xl mt-4 mb-1.5 font-bold; + } + h3 { + @apply text-2xl mt-5 mb-1.5 font-bold; + } + h4 { + @apply text-xl mt-6 mb-1.5 font-bold; + } + p { + @apply mb-2; + } + a { + @apply text-[#FFC832] underline hover:decoration-orange-400 transition duration-300; + } + ul { + @apply mt-2 mb-3 ml-1 list-disc list-inside marker:text-sky-600; + } + ol { + @apply mt-2 mb-3 ml-1 list-decimal list-inside marker:text-sky-500; + } + li { + @apply my-0.5; + } + code { + @apply bg-white/10 px-1 pb-px pt-1 rounded-md; + } + pre code { + @apply bg-inherit p-0 text-inherit; + } + hr { + @apply my-5 rounded-full; + } + img { + @apply md:w-3/4 lg:w-3/5; + } + blockquote { + @apply px-3 pt-2 pb-px mb-4 mt-2 border-s-4 border-white/80 bg-white/7 rounded-sm; + } + + pre { + @apply px-2 pt-2 pb-1.5 overflow-x-auto text-sm sm:text-base rounded-sm mt-2 mb-4 selection:bg-white/15; + } +} diff --git a/website/justfile b/website/justfile new file mode 100644 index 00000000..7efc3ef9 --- /dev/null +++ b/website/justfile @@ -0,0 +1,5 @@ +zola: + zola serve --open + +tailwind: + npx @tailwindcss/cli -w -i input.css -o static/main.css diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..80c84872 --- /dev/null +++ b/website/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@tailwindcss/cli": "^4" + } +} diff --git a/website/static/images/happy_ferris.svg b/website/static/images/happy_ferris.svg new file mode 100644 index 00000000..c7f240dd --- /dev/null +++ b/website/static/images/happy_ferris.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/images/panic.svg b/website/static/images/panic.svg new file mode 100644 index 00000000..be55f5e0 --- /dev/null +++ b/website/static/images/panic.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/images/rust_logo.svg b/website/static/images/rust_logo.svg new file mode 100644 index 00000000..3b42cfe0 --- /dev/null +++ b/website/static/images/rust_logo.svg @@ -0,0 +1,61 @@ + + + diff --git a/website/templates/404.html b/website/templates/404.html new file mode 100644 index 00000000..eb9d4691 --- /dev/null +++ b/website/templates/404.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +

+

DON'T PANIC!

+

404: Page not found!

+ + + + Back to homepage +
+{% endblock %} diff --git a/website/templates/anchor-link.html b/website/templates/anchor-link.html new file mode 100644 index 00000000..c8644d96 --- /dev/null +++ b/website/templates/anchor-link.html @@ -0,0 +1,2 @@ + diff --git a/website/templates/base.html b/website/templates/base.html new file mode 100644 index 00000000..1a55aebf --- /dev/null +++ b/website/templates/base.html @@ -0,0 +1,92 @@ + + + + + + + {%- set timestamp = now(timestamp=true) -%} + + {%- if page.title -%} + {% set_global title = page.title %} + {%- elif section.title -%} + {% set_global title = section.title %} + {%- else -%} + {% set_global title = config.title %} + {%- endif -%} + + {%- if page.description -%} + {% set_global description = page.description %} + {%- elif section.description -%} + {% set_global description = section.description %} + {%- else -%} + {% set_global description = config.description %} + {%- endif -%} + + {%- if page.permalink -%} + {% set_global permalink = page.permalink %} + {%- elif section.permalink -%} + {% set_global permalink = section.permalink %} + {%- endif %} + + {%- block title -%}{{- title -}}{%- endblock -%} + + + + + + + + + + + {% if permalink %}{% endif %} + + + +
+ + + +
+ +
+ {% block content %}{% endblock %} +
+ +
+
+ +
Rustlings is an official Rust project
+
+ + +
+ + diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 00000000..0d2b2e39 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
+

Rustlings

+ + {{ section.content | safe }} +
+{% endblock %} diff --git a/website/templates/page.html b/website/templates/page.html new file mode 100644 index 00000000..b2f6c019 --- /dev/null +++ b/website/templates/page.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{ page.title }}

+ +
+ +
+ + {{ page.content | safe }} +
+{% endblock %} diff --git a/website/templates/shortcodes/details.html b/website/templates/shortcodes/details.html new file mode 100644 index 00000000..1c07778c --- /dev/null +++ b/website/templates/shortcodes/details.html @@ -0,0 +1,9 @@ +
+ + {{ summary | safe }} (click to expand) + + +
+ {{ body | markdown | safe }} +
+