summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMo <76752051+mo8it@users.noreply.github.com>2024-04-14 17:13:32 +0200
committerGitHub <noreply@github.com>2024-04-14 17:13:32 +0200
commitdc02c38a945fcafacf6d2d35f5d3e317e7185cb0 (patch)
treebd3ad843a575650881b220c4b008fc7509917d24
parent8c8f30d8ce3b732de649938d8945496bd769ac22 (diff)
parent7526c6b1f92626df6ab8b4853535b73711bfada4 (diff)
Merge pull request #1942 from rust-lang/tui
TUI
-rw-r--r--.devcontainer/devcontainer.json8
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore25
-rw-r--r--.gitpod.yml7
-rw-r--r--.vscode/extensions.json5
-rw-r--r--Cargo.lock436
-rw-r--r--Cargo.toml23
-rw-r--r--README.md85
-rw-r--r--dev/Cargo.toml6
-rw-r--r--exercises/00_intro/intro1.rs14
-rw-r--r--exercises/00_intro/intro2.rs2
-rw-r--r--exercises/01_variables/variables1.rs2
-rw-r--r--exercises/01_variables/variables2.rs2
-rw-r--r--exercises/01_variables/variables3.rs2
-rw-r--r--exercises/01_variables/variables4.rs2
-rw-r--r--exercises/01_variables/variables5.rs2
-rw-r--r--exercises/01_variables/variables6.rs2
-rw-r--r--exercises/02_functions/functions1.rs2
-rw-r--r--exercises/02_functions/functions2.rs2
-rw-r--r--exercises/02_functions/functions3.rs2
-rw-r--r--exercises/02_functions/functions4.rs2
-rw-r--r--exercises/02_functions/functions5.rs2
-rw-r--r--exercises/03_if/if1.rs2
-rw-r--r--exercises/03_if/if2.rs2
-rw-r--r--exercises/03_if/if3.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types1.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types2.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types3.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types4.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types5.rs2
-rw-r--r--exercises/04_primitive_types/primitive_types6.rs2
-rw-r--r--exercises/05_vecs/vecs1.rs2
-rw-r--r--exercises/05_vecs/vecs2.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics1.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics2.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics3.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics4.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics5.rs2
-rw-r--r--exercises/06_move_semantics/move_semantics6.rs2
-rw-r--r--exercises/07_structs/structs1.rs2
-rw-r--r--exercises/07_structs/structs2.rs2
-rw-r--r--exercises/07_structs/structs3.rs2
-rw-r--r--exercises/08_enums/enums1.rs2
-rw-r--r--exercises/08_enums/enums2.rs2
-rw-r--r--exercises/08_enums/enums3.rs2
-rw-r--r--exercises/09_strings/strings1.rs2
-rw-r--r--exercises/09_strings/strings2.rs2
-rw-r--r--exercises/09_strings/strings3.rs2
-rw-r--r--exercises/09_strings/strings4.rs2
-rw-r--r--exercises/10_modules/modules1.rs2
-rw-r--r--exercises/10_modules/modules2.rs2
-rw-r--r--exercises/10_modules/modules3.rs2
-rw-r--r--exercises/11_hashmaps/hashmaps1.rs2
-rw-r--r--exercises/11_hashmaps/hashmaps2.rs2
-rw-r--r--exercises/11_hashmaps/hashmaps3.rs2
-rw-r--r--exercises/12_options/options1.rs2
-rw-r--r--exercises/12_options/options2.rs2
-rw-r--r--exercises/12_options/options3.rs2
-rw-r--r--exercises/13_error_handling/errors1.rs2
-rw-r--r--exercises/13_error_handling/errors2.rs2
-rw-r--r--exercises/13_error_handling/errors3.rs2
-rw-r--r--exercises/13_error_handling/errors4.rs2
-rw-r--r--exercises/13_error_handling/errors5.rs2
-rw-r--r--exercises/13_error_handling/errors6.rs2
-rw-r--r--exercises/14_generics/generics1.rs2
-rw-r--r--exercises/14_generics/generics2.rs2
-rw-r--r--exercises/15_traits/traits1.rs2
-rw-r--r--exercises/15_traits/traits2.rs2
-rw-r--r--exercises/15_traits/traits3.rs2
-rw-r--r--exercises/15_traits/traits4.rs2
-rw-r--r--exercises/15_traits/traits5.rs2
-rw-r--r--exercises/16_lifetimes/lifetimes1.rs2
-rw-r--r--exercises/16_lifetimes/lifetimes2.rs2
-rw-r--r--exercises/16_lifetimes/lifetimes3.rs2
-rw-r--r--exercises/17_tests/tests1.rs2
-rw-r--r--exercises/17_tests/tests2.rs2
-rw-r--r--exercises/17_tests/tests3.rs2
-rw-r--r--exercises/17_tests/tests4.rs2
-rw-r--r--exercises/18_iterators/iterators1.rs2
-rw-r--r--exercises/18_iterators/iterators2.rs2
-rw-r--r--exercises/18_iterators/iterators3.rs2
-rw-r--r--exercises/18_iterators/iterators4.rs2
-rw-r--r--exercises/18_iterators/iterators5.rs2
-rw-r--r--exercises/19_smart_pointers/arc1.rs2
-rw-r--r--exercises/19_smart_pointers/box1.rs2
-rw-r--r--exercises/19_smart_pointers/cow1.rs2
-rw-r--r--exercises/19_smart_pointers/rc1.rs2
-rw-r--r--exercises/20_threads/threads1.rs2
-rw-r--r--exercises/20_threads/threads2.rs2
-rw-r--r--exercises/20_threads/threads3.rs2
-rw-r--r--exercises/21_macros/macros1.rs2
-rw-r--r--exercises/21_macros/macros2.rs2
-rw-r--r--exercises/21_macros/macros3.rs2
-rw-r--r--exercises/21_macros/macros4.rs2
-rw-r--r--exercises/22_clippy/clippy1.rs2
-rw-r--r--exercises/22_clippy/clippy2.rs2
-rw-r--r--exercises/22_clippy/clippy3.rs2
-rw-r--r--exercises/23_conversions/as_ref_mut.rs2
-rw-r--r--exercises/23_conversions/from_into.rs2
-rw-r--r--exercises/23_conversions/from_str.rs2
-rw-r--r--exercises/23_conversions/try_from_into.rs2
-rw-r--r--exercises/23_conversions/using_as.rs2
-rw-r--r--exercises/quiz1.rs2
-rw-r--r--exercises/quiz2.rs2
-rw-r--r--exercises/quiz3.rs2
-rw-r--r--flake.lock78
-rw-r--r--flake.nix78
-rw-r--r--gen-dev-cargo-toml/Cargo.toml10
-rw-r--r--gen-dev-cargo-toml/src/main.rs (renamed from src/bin/gen-dev-cargo-toml.rs)32
-rw-r--r--info.toml312
-rw-r--r--rustlings-macros/Cargo.toml2
-rw-r--r--rustlings-macros/src/lib.rs1
-rw-r--r--shell.nix6
-rw-r--r--src/app_state.rs277
-rw-r--r--src/embedded.rs8
-rw-r--r--src/exercise.rs294
-rw-r--r--src/info_file.rs79
-rw-r--r--src/init.rs63
-rw-r--r--src/list.rs90
-rw-r--r--src/list/state.rs261
-rw-r--r--src/main.rs484
-rw-r--r--src/progress_bar.rs97
-rw-r--r--src/run.rs60
-rw-r--r--src/ui.rs28
-rw-r--r--src/verify.rs223
-rw-r--r--src/watch.rs128
-rw-r--r--src/watch/notify_event.rs42
-rw-r--r--src/watch/state.rs168
-rw-r--r--src/watch/terminal_event.rs73
-rw-r--r--tests/dev_cargo_bins.rs41
-rw-r--r--tests/fixture/failure/Cargo.toml2
-rw-r--r--tests/fixture/failure/info.toml4
-rw-r--r--tests/fixture/state/Cargo.toml2
-rw-r--r--tests/fixture/state/exercises/pending_exercise.rs2
-rw-r--r--tests/fixture/state/exercises/pending_test_exercise.rs2
-rw-r--r--tests/fixture/state/info.toml7
-rw-r--r--tests/fixture/success/Cargo.toml2
-rw-r--r--tests/fixture/success/info.toml4
-rw-r--r--tests/integration_tests.rs134
139 files changed, 2040 insertions, 1855 deletions
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
deleted file mode 100644
index f25e8bd..0000000
--- a/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "image": "mcr.microsoft.com/devcontainers/rust:1",
- "updateContentCommand": ["cargo", "build"],
- "postAttachCommand": ["rustlings", "watch"],
- "remoteEnv": {
- "PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug"
- }
-}
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index efdba87..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,2 +0,0 @@
-* text=auto
-*.sh text eol=lf
diff --git a/.gitignore b/.gitignore
index 0bbbc54..80f9092 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,18 +1,23 @@
+# Cargo
target/
/tests/fixture/*/Cargo.lock
/dev/Cargo.lock
-*.swp
-**/*.rs.bk
+# State file
+.rustlings-state.txt
+
+# oranda
+public/
+.netlify
+
+# OS
.DS_Store
-*.pdb
+.direnv/
+
+# Editor
+*.swp
.idea
-.vscode/*
-!.vscode/extensions.json
*.iml
-*.o
-public/
-.direnv/
-# Local Netlify folder
-.netlify
+# Ignore file for editors like Helix
+.ignore
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 0691933..0000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-tasks:
- - init: /workspace/rustlings/install.sh
- command: /workspace/.cargo/bin/rustlings watch
-
-vscode:
- extensions:
- - rust-lang.rust-analyzer@0.3.1348
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index b85de74..0000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "recommendations": [
- "rust-lang.rust-analyzer"
- ]
-}
diff --git a/Cargo.lock b/Cargo.lock
index d8e5b72..5cfebe6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,18 @@
version = 3
[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -12,6 +24,12 @@ dependencies = [
]
[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
+[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -61,9 +79,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.81"
+version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "assert_cmd"
@@ -110,6 +128,21 @@ dependencies = [
]
[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -143,10 +176,10 @@ version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
- "heck",
+ "heck 0.5.0",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.58",
]
[[package]]
@@ -162,16 +195,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
-name = "console"
-version = "0.15.8"
+name = "compact_str"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
- "encode_unicode",
- "lazy_static",
- "libc",
- "unicode-width",
- "windows-sys 0.52.0",
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "ryu",
+ "static_assertions",
]
[[package]]
@@ -190,6 +223,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
+name = "crossterm"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+dependencies = [
+ "bitflags 2.5.0",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -203,15 +261,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
-
-[[package]]
-name = "encode_unicode"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
[[package]]
name = "equivalent"
@@ -260,16 +312,29 @@ dependencies = [
]
[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+name = "gen-dev-cargo-toml"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "serde",
+ "toml_edit",
+]
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
@@ -297,17 +362,10 @@ dependencies = [
]
[[package]]
-name = "indicatif"
-version = "0.17.8"
+name = "indoc"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
-dependencies = [
- "console",
- "instant",
- "number_prefix",
- "portable-atomic",
- "unicode-width",
-]
+checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inotify"
@@ -330,12 +388,12 @@ dependencies = [
]
[[package]]
-name = "instant"
-version = "0.1.12"
+name = "itertools"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
- "cfg-if",
+ "either",
]
[[package]]
@@ -365,12 +423,6 @@ dependencies = [
]
[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
-[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -383,16 +435,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
+name = "lru"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
name = "memchr"
-version = "2.7.1"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "mio"
@@ -452,16 +523,39 @@ dependencies = [
]
[[package]]
-name = "number_prefix"
-version = "0.4.0"
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
[[package]]
-name = "portable-atomic"
-version = "1.6.0"
+name = "paste"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "predicates"
@@ -504,14 +598,34 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.35"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
+name = "ratatui"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
+dependencies = [
+ "bitflags 2.5.0",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "indoc",
+ "itertools",
+ "lru",
+ "paste",
+ "stability",
+ "strum",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -569,18 +683,15 @@ dependencies = [
"anyhow",
"assert_cmd",
"clap",
- "console",
- "glob",
- "indicatif",
+ "crossterm",
+ "hashbrown",
"notify-debouncer-mini",
"predicates",
+ "ratatui",
"rustlings-macros",
"serde",
- "serde_json",
- "shlex",
"toml_edit",
"which",
- "winnow",
]
[[package]]
@@ -591,6 +702,12 @@ dependencies = [
]
[[package]]
+name = "rustversion"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
+
+[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -606,6 +723,12 @@ dependencies = [
]
[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -622,46 +745,114 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.58",
]
[[package]]
-name = "serde_json"
-version = "1.0.115"
+name = "serde_spanned"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
- "itoa",
- "ryu",
"serde",
]
[[package]]
-name = "serde_spanned"
-version = "0.6.5"
+name = "signal-hook"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
- "serde",
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "stability"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
]
[[package]]
-name = "shlex"
-version = "1.3.0"
+name = "static_assertions"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
-version = "0.11.0"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.58",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
[[package]]
name = "syn"
-version = "2.0.55"
+version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
+checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
@@ -703,6 +894,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -715,6 +912,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -797,7 +1000,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
]
[[package]]
@@ -817,17 +1020,18 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
- "windows_aarch64_gnullvm 0.52.4",
- "windows_aarch64_msvc 0.52.4",
- "windows_i686_gnu 0.52.4",
- "windows_i686_msvc 0.52.4",
- "windows_x86_64_gnu 0.52.4",
- "windows_x86_64_gnullvm 0.52.4",
- "windows_x86_64_msvc 0.52.4",
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
]
[[package]]
@@ -838,9 +1042,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
@@ -850,9 +1054,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
@@ -862,9 +1066,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
-version = "0.52.4"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
@@ -874,9 +1084,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
@@ -886,9 +1096,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -898,9 +1108,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
@@ -910,15 +1120,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.52.4"
+version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
-version = "0.6.5"
+version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
+checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
dependencies = [
"memchr",
]
@@ -928,3 +1138,23 @@ name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.58",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 86187b4..07865ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,9 @@ exclude = [
"tests/fixture/success",
"dev",
]
+members = [
+ "gen-dev-cargo-toml",
+]
[workspace.package]
version = "6.0.0"
@@ -16,6 +19,11 @@ authors = [
license = "MIT"
edition = "2021"
+[workspace.dependencies]
+anyhow = "1.0.82"
+serde = { version = "1.0.197", features = ["derive"] }
+toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
+
[package]
name = "rustlings"
description = "Small exercises to get you used to reading and writing Rust code!"
@@ -26,22 +34,19 @@ license.workspace = true
edition.workspace = true
[dependencies]
-anyhow = "1.0.81"
+anyhow.workspace = true
clap = { version = "4.5.4", features = ["derive"] }
-console = "0.15.8"
-indicatif = "0.17.8"
+crossterm = "0.27.0"
+hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1"
+ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" }
-serde_json = "1.0.115"
-serde = { version = "1.0.197", features = ["derive"] }
-shlex = "1.3.0"
-toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
+serde.workspace = true
+toml_edit.workspace = true
which = "6.0.1"
-winnow = "0.6.5"
[dev-dependencies]
assert_cmd = "2.0.14"
-glob = "0.3.0"
predicates = "3.1.0"
[profile.release]
diff --git a/README.md b/README.md
index 6b9c983..96421eb 100644
--- a/README.md
+++ b/README.md
@@ -18,78 +18,7 @@ _Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt instal
You will need to have Rust installed. You can get it by visiting <https://rustup.rs>. This'll also install Cargo, Rust's package/project manager.
-## MacOS/Linux
-
-Just run:
-
-```bash
-curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash
-```
-
-Or if you want it to be installed to a different path:
-
-```bash
-curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s mypath/
-```
-
-This will install Rustlings and give you access to the `rustlings` command. Run it to get started!
-
-### Nix
-
-Basically: Clone the repository at the latest tag, finally run `nix develop` or `nix-shell`.
-
-```bash
-# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
-git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
-cd rustlings
-# if nix version > 2.3
-nix develop
-# if nix version <= 2.3
-nix-shell
-```
-
-## Windows
-
-In PowerShell (Run as Administrator), set `ExecutionPolicy` to `RemoteSigned`:
-
-```ps1
-Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
-```
-
-Then, you can run:
-
-```ps1
-Start-BitsTransfer -Source https://raw.githubusercontent.com/rust-lang/rustlings/main/install.ps1 -Destination $env:TMP/install_rustlings.ps1; Unblock-File $env:TMP/install_rustlings.ps1; Invoke-Expression $env:TMP/install_rustlings.ps1
-```
-
-To install Rustlings. Same as on MacOS/Linux, you will have access to the `rustlings` command after it. Keep in mind that this works best in PowerShell, and any other terminals may give you errors.
-
-If you get a permission denied message, you might have to exclude the directory where you cloned Rustlings in your antivirus.
-
-## Browser
-
-[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rust-lang/rustlings)
-
-[![Open Rustlings On Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/?repo=rust-lang%2Frustlings&ref=main)
-
-## Manually
-
-Basically: Clone the repository at the latest tag, run `cargo install --path .`.
-
-```bash
-# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1)
-git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
-cd rustlings
-cargo install --force --path .
-```
-
-If there are installation errors, ensure that your toolchain is up to date. For the latest, run:
-
-```bash
-rustup update
-```
-
-Then, same as above, run `rustlings` to get started.
+<!-- TODO: Installation with Cargo -->
## Doing exercises
@@ -101,13 +30,7 @@ The task is simple. Most exercises contain an error that keeps them from compili
rustlings watch
```
-This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. If you want to only run it once, you can use:
-
-```bash
-rustlings verify
-```
-
-This will do the same as watch, but it'll quit after running.
+This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory.
In case you want to go by your own order, or want to only verify a single exercise, you can run:
@@ -144,10 +67,6 @@ rustlings list
After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`.
-## Enabling `rust-analyzer`
-
-Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise.
-
## 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.
diff --git a/dev/Cargo.toml b/dev/Cargo.toml
index 7868b97..1d230eb 100644
--- a/dev/Cargo.toml
+++ b/dev/Cargo.toml
@@ -1,5 +1,5 @@
-# This file is a hack to allow using `cargo r` to test `rustlings` during development.
-# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`.
+# This file is a hack to allow using `cargo run` to test `rustlings` during development.
+# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`.
bin = [
{ name = "intro1", path = "../exercises/00_intro/intro1.rs" },
@@ -101,6 +101,6 @@ bin = [
]
[package]
-name = "rustlings"
+name = "rustlings-dev"
edition = "2021"
publish = false
diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs
index 5dd18b4..170d195 100644
--- a/exercises/00_intro/intro1.rs
+++ b/exercises/00_intro/intro1.rs
@@ -1,6 +1,5 @@
// intro1.rs
//
-// About this `I AM NOT DONE` thing:
// We sometimes encourage you to keep trying things on a given exercise, even
// after you already figured it out. If you got everything working and feel
// ready for the next exercise, remove the `I AM NOT DONE` comment below.
@@ -13,8 +12,6 @@
// Execute `rustlings hint intro1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
println!("Hello and");
println!(r#" welcome to... "#);
@@ -29,13 +26,6 @@ fn main() {
println!("or logic error. The central concept behind Rustlings is to fix these errors and");
println!("solve the exercises. Good luck!");
println!();
- println!("The source for this exercise is in `exercises/00_intro/intro1.rs`. Have a look!");
- println!(
- "Going forward, the source of the exercises will always be in the success/failure output."
- );
- println!();
- println!(
- "If you want to use rust-analyzer, Rust's LSP implementation, make sure your editor is set"
- );
- println!("up, and then run `rustlings lsp` before continuing.")
+ println!("The file of this exercise is `exercises/00_intro/intro1.rs`. Have a look!");
+ println!("The current exercise path is shown under the progress bar in the watch mode.");
}
diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs
index a28ad3d..84e0d75 100644
--- a/exercises/00_intro/intro2.rs
+++ b/exercises/00_intro/intro2.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint intro2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
printline!("Hello there!")
}
diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs
index b3e089a..56408f3 100644
--- a/exercises/01_variables/variables1.rs
+++ b/exercises/01_variables/variables1.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint variables1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
x = 5;
println!("x has the value {}", x);
diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs
index e1c23ed..0f417e0 100644
--- a/exercises/01_variables/variables2.rs
+++ b/exercises/01_variables/variables2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint variables2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let x;
if x == 10 {
diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs
index 86bed41..421c6b1 100644
--- a/exercises/01_variables/variables3.rs
+++ b/exercises/01_variables/variables3.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint variables3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let x: i32;
println!("Number {}", x);
diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs
index 5394f39..68f8f50 100644
--- a/exercises/01_variables/variables4.rs
+++ b/exercises/01_variables/variables4.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint variables4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let x = 3;
println!("Number {}", x);
diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs
index a29b38b..7014c56 100644
--- a/exercises/01_variables/variables5.rs
+++ b/exercises/01_variables/variables5.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint variables5` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let number = "T-H-R-E-E"; // don't change this line
println!("Spell a Number : {}", number);
diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs
index 853183b..9f47682 100644
--- a/exercises/01_variables/variables6.rs
+++ b/exercises/01_variables/variables6.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint variables6` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
const NUMBER = 3;
fn main() {
println!("Number {}", NUMBER);
diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs
index 40ed9a0..2365f91 100644
--- a/exercises/02_functions/functions1.rs
+++ b/exercises/02_functions/functions1.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint functions1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
call_me();
}
diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs
index 5154f34..64dbd66 100644
--- a/exercises/02_functions/functions2.rs
+++ b/exercises/02_functions/functions2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint functions2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
call_me(3);
}
diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs
index 74f44d6..5037121 100644
--- a/exercises/02_functions/functions3.rs
+++ b/exercises/02_functions/functions3.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint functions3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
call_me();
}
diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs
index 77c4b2a..6b449ed 100644
--- a/exercises/02_functions/functions4.rs
+++ b/exercises/02_functions/functions4.rs
@@ -8,8 +8,6 @@
// Execute `rustlings hint functions4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let original_price = 51;
println!("Your sale price is {}", sale_price(original_price));
diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs
index f1b63f4..0c96322 100644
--- a/exercises/02_functions/functions5.rs
+++ b/exercises/02_functions/functions5.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint functions5` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let answer = square(3);
println!("The square of 3 is {}", answer);
diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs
index d2afccf..a1df66b 100644
--- a/exercises/03_if/if1.rs
+++ b/exercises/03_if/if1.rs
@@ -2,8 +2,6 @@
//
// Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
pub fn bigger(a: i32, b: i32) -> i32 {
// Complete this function to return the bigger number!
// If both numbers are equal, any of them can be returned.
diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs
index f512f13..7b9c05f 100644
--- a/exercises/03_if/if2.rs
+++ b/exercises/03_if/if2.rs
@@ -5,8 +5,6 @@
//
// Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
pub fn foo_if_fizz(fizzish: &str) -> &str {
if fizzish == "fizz" {
"foo"
diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs
index 1696274..caba172 100644
--- a/exercises/03_if/if3.rs
+++ b/exercises/03_if/if3.rs
@@ -2,8 +2,6 @@
//
// Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
pub fn animal_habitat(animal: &str) -> &'static str {
let identifier = if animal == "crab" {
1
diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs
index 3663340..f9169c8 100644
--- a/exercises/04_primitive_types/primitive_types1.rs
+++ b/exercises/04_primitive_types/primitive_types1.rs
@@ -3,8 +3,6 @@
// Fill in the rest of the line that has code missing! No hints, there's no
// tricks, just get used to typing these :)
-// I AM NOT DONE
-
fn main() {
// Booleans (`bool`)
diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs
index f1616ed..1911b12 100644
--- a/exercises/04_primitive_types/primitive_types2.rs
+++ b/exercises/04_primitive_types/primitive_types2.rs
@@ -3,8 +3,6 @@
// Fill in the rest of the line that has code missing! No hints, there's no
// tricks, just get used to typing these :)
-// I AM NOT DONE
-
fn main() {
// Characters (`char`)
diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs
index 8b0de44..70a8cc2 100644
--- a/exercises/04_primitive_types/primitive_types3.rs
+++ b/exercises/04_primitive_types/primitive_types3.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
fn main() {
let a = ???
diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs
index d44d877..8ed0a82 100644
--- a/exercises/04_primitive_types/primitive_types4.rs
+++ b/exercises/04_primitive_types/primitive_types4.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn slice_out_of_array() {
let a = [1, 2, 3, 4, 5];
diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs
index f646986..5754a3d 100644
--- a/exercises/04_primitive_types/primitive_types5.rs
+++ b/exercises/04_primitive_types/primitive_types5.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
fn main() {
let cat = ("Furry McFurson", 3.5);
let /* your pattern here */ = cat;
diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs
index 07cc46c..5f82f10 100644
--- a/exercises/04_primitive_types/primitive_types6.rs
+++ b/exercises/04_primitive_types/primitive_types6.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn indexing_tuple() {
let numbers = (1, 2, 3);
diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs
index 65b7a7f..c64acbb 100644
--- a/exercises/05_vecs/vecs1.rs
+++ b/exercises/05_vecs/vecs1.rs
@@ -7,8 +7,6 @@
//
// Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
fn array_and_vec() -> ([i32; 4], Vec<i32>) {
let a = [10, 20, 30, 40]; // a plain array
let v = // TODO: declare your vector here with the macro for vectors
diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs
index e92c970..d64d3d1 100644
--- a/exercises/05_vecs/vecs2.rs
+++ b/exercises/05_vecs/vecs2.rs
@@ -7,8 +7,6 @@
//
// Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
fn vec_loop(mut v: Vec<i32>) -> Vec<i32> {
for element in v.iter_mut() {
// TODO: Fill this up so that each element in the Vec `v` is
diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs
index e063937..c612ba9 100644
--- a/exercises/06_move_semantics/move_semantics1.rs
+++ b/exercises/06_move_semantics/move_semantics1.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs
index dc58be5..3457d11 100644
--- a/exercises/06_move_semantics/move_semantics2.rs
+++ b/exercises/06_move_semantics/move_semantics2.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs
index 7152c71..9415eb1 100644
--- a/exercises/06_move_semantics/move_semantics3.rs
+++ b/exercises/06_move_semantics/move_semantics3.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs
index bfc917f..1509f5d 100644
--- a/exercises/06_move_semantics/move_semantics4.rs
+++ b/exercises/06_move_semantics/move_semantics4.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let vec0 = vec![22, 44, 66];
diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs
index 267bdcc..c84d2fe 100644
--- a/exercises/06_move_semantics/move_semantics5.rs
+++ b/exercises/06_move_semantics/move_semantics5.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let mut x = 100;
diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs
index cace4ca..6059e61 100644
--- a/exercises/06_move_semantics/move_semantics6.rs
+++ b/exercises/06_move_semantics/move_semantics6.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand
// for a hint.
-// I AM NOT DONE
-
fn main() {
let data = "Rust is great!".to_string();
diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs
index 5fa5821..2978121 100644
--- a/exercises/07_structs/structs1.rs
+++ b/exercises/07_structs/structs1.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint structs1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
struct ColorClassicStruct {
// TODO: Something goes here
}
diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs
index 328567f..a7a2dec 100644
--- a/exercises/07_structs/structs2.rs
+++ b/exercises/07_structs/structs2.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint structs2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[derive(Debug)]
struct Order {
name: String,
diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs
index 7cda5af..9835b81 100644
--- a/exercises/07_structs/structs3.rs
+++ b/exercises/07_structs/structs3.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint structs3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[derive(Debug)]
struct Package {
sender_country: String,
diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs
index 25525b2..330269c 100644
--- a/exercises/08_enums/enums1.rs
+++ b/exercises/08_enums/enums1.rs
@@ -2,8 +2,6 @@
//
// No hints this time! ;)
-// I AM NOT DONE
-
#[derive(Debug)]
enum Message {
// TODO: define a few types of messages as used below
diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs
index df93fe0..f0e4e6d 100644
--- a/exercises/08_enums/enums2.rs
+++ b/exercises/08_enums/enums2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint enums2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[derive(Debug)]
enum Message {
// TODO: define the different variants used below
diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs
index 92d18c4..580a553 100644
--- a/exercises/08_enums/enums3.rs
+++ b/exercises/08_enums/enums3.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint enums3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
enum Message {
// TODO: implement the message variant types based on their usage below
}
diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs
index f50e1fa..a1255a3 100644
--- a/exercises/09_strings/strings1.rs
+++ b/exercises/09_strings/strings1.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint strings1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let answer = current_favorite_color();
println!("My current favorite color is {}", answer);
diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs
index 4d95d16..ba76fe6 100644
--- a/exercises/09_strings/strings2.rs
+++ b/exercises/09_strings/strings2.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint strings2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let word = String::from("green"); // Try not changing this line :)
if is_a_color_word(word) {
diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs
index 384e7ce..dedc081 100644
--- a/exercises/09_strings/strings3.rs
+++ b/exercises/09_strings/strings3.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint strings3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn trim_me(input: &str) -> String {
// TODO: Remove whitespace from both ends of a string!
???
diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs
index e8c54ac..a034aa4 100644
--- a/exercises/09_strings/strings4.rs
+++ b/exercises/09_strings/strings4.rs
@@ -7,8 +7,6 @@
//
// No hints this time!
-// I AM NOT DONE
-
fn string_slice(arg: &str) {
println!("{}", arg);
}
diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs
index 9eb5a48..c750946 100644
--- a/exercises/10_modules/modules1.rs
+++ b/exercises/10_modules/modules1.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint modules1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
mod sausage_factory {
// Don't let anybody outside of this module see this!
fn get_secret_recipe() -> String {
diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs
index 0415454..4d3106c 100644
--- a/exercises/10_modules/modules2.rs
+++ b/exercises/10_modules/modules2.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint modules2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
mod delicious_snacks {
// TODO: Fix these use statements
use self::fruits::PEAR as ???
diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs
index f2bb050..c211a76 100644
--- a/exercises/10_modules/modules3.rs
+++ b/exercises/10_modules/modules3.rs
@@ -8,8 +8,6 @@
// Execute `rustlings hint modules3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
// TODO: Complete this use statement
use ???
diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs
index 80829ea..5a52f61 100644
--- a/exercises/11_hashmaps/hashmaps1.rs
+++ b/exercises/11_hashmaps/hashmaps1.rs
@@ -11,8 +11,6 @@
// Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::collections::HashMap;
fn fruit_basket() -> HashMap<String, u32> {
diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs
index a592569..2730643 100644
--- a/exercises/11_hashmaps/hashmaps2.rs
+++ b/exercises/11_hashmaps/hashmaps2.rs
@@ -14,8 +14,6 @@
// Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::collections::HashMap;
#[derive(Hash, PartialEq, Eq)]
diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs
index 8d9236d..775a401 100644
--- a/exercises/11_hashmaps/hashmaps3.rs
+++ b/exercises/11_hashmaps/hashmaps3.rs
@@ -15,8 +15,6 @@
// Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::collections::HashMap;
// A structure to store the goal details of a team.
diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs
index 3cbfecd..ba4b1cd 100644
--- a/exercises/12_options/options1.rs
+++ b/exercises/12_options/options1.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint options1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
// This function returns how much icecream there is left in the fridge.
// If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it
// all, so there'll be no more left :(
diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs
index 4d998e7..73f707e 100644
--- a/exercises/12_options/options2.rs
+++ b/exercises/12_options/options2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint options2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[cfg(test)]
mod tests {
#[test]
diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs
index 23c15ea..7922ef9 100644
--- a/exercises/12_options/options3.rs
+++ b/exercises/12_options/options3.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint options3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
struct Point {
x: i32,
y: i32,
diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs
index 0ba59a5..9767f2c 100644
--- a/exercises/13_error_handling/errors1.rs
+++ b/exercises/13_error_handling/errors1.rs
@@ -9,8 +9,6 @@
// Execute `rustlings hint errors1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub fn generate_nametag_text(name: String) -> Option<String> {
if name.is_empty() {
// Empty names aren't allowed.
diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs
index 631fe67..88d1bf4 100644
--- a/exercises/13_error_handling/errors2.rs
+++ b/exercises/13_error_handling/errors2.rs
@@ -19,8 +19,6 @@
// Execute `rustlings hint errors2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::num::ParseIntError;
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs
index d42d3b1..56bb31b 100644
--- a/exercises/13_error_handling/errors3.rs
+++ b/exercises/13_error_handling/errors3.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint errors3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::num::ParseIntError;
fn main() {
diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs
index d6d6fcb..0e5c08b 100644
--- a/exercises/13_error_handling/errors4.rs
+++ b/exercises/13_error_handling/errors4.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint errors4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs
index 92461a7..0bcb4b8 100644
--- a/exercises/13_error_handling/errors5.rs
+++ b/exercises/13_error_handling/errors5.rs
@@ -22,8 +22,6 @@
// Execute `rustlings hint errors5` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::error;
use std::fmt;
use std::num::ParseIntError;
diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs
index aaf0948..de73a9a 100644
--- a/exercises/13_error_handling/errors6.rs
+++ b/exercises/13_error_handling/errors6.rs
@@ -9,8 +9,6 @@
// Execute `rustlings hint errors6` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::num::ParseIntError;
// This is a custom error type that we will be using in `parse_pos_nonzero()`.
diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs
index 35c1d2f..545fd95 100644
--- a/exercises/14_generics/generics1.rs
+++ b/exercises/14_generics/generics1.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint generics1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let mut shopping_list: Vec<?> = Vec::new();
shopping_list.push("milk");
diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs
index 074cd93..d50ed17 100644
--- a/exercises/14_generics/generics2.rs
+++ b/exercises/14_generics/generics2.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint generics2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
struct Wrapper {
value: u32,
}
diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs
index 37dfcbf..c51d3b8 100644
--- a/exercises/15_traits/traits1.rs
+++ b/exercises/15_traits/traits1.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint traits1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
trait AppendBar {
fn append_bar(self) -> Self;
}
diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs
index 3e35f8e..9a2bc07 100644
--- a/exercises/15_traits/traits2.rs
+++ b/exercises/15_traits/traits2.rs
@@ -8,8 +8,6 @@
//
// Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
trait AppendBar {
fn append_bar(self) -> Self;
}
diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs
index 4e2b06b..357f1d7 100644
--- a/exercises/15_traits/traits3.rs
+++ b/exercises/15_traits/traits3.rs
@@ -8,8 +8,6 @@
// Execute `rustlings hint traits3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub trait Licensed {
fn licensing_info(&self) -> String;
}
diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs
index 4bda3e5..7242c48 100644
--- a/exercises/15_traits/traits4.rs
+++ b/exercises/15_traits/traits4.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint traits4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub trait Licensed {
fn licensing_info(&self) -> String {
"some information".to_string()
diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs
index df18380..f258d32 100644
--- a/exercises/15_traits/traits5.rs
+++ b/exercises/15_traits/traits5.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint traits5` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub trait SomeTrait {
fn some_function(&self) -> bool {
true
diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs
index 87bde49..4f544b4 100644
--- a/exercises/16_lifetimes/lifetimes1.rs
+++ b/exercises/16_lifetimes/lifetimes1.rs
@@ -8,8 +8,6 @@
// Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs
index 4f3d8c1..33b5565 100644
--- a/exercises/16_lifetimes/lifetimes2.rs
+++ b/exercises/16_lifetimes/lifetimes2.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs
index 9c59f9c..de6005e 100644
--- a/exercises/16_lifetimes/lifetimes3.rs
+++ b/exercises/16_lifetimes/lifetimes3.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
struct Book {
author: &str,
title: &str,
diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs
index 810277a..bde2108 100644
--- a/exercises/17_tests/tests1.rs
+++ b/exercises/17_tests/tests1.rs
@@ -10,8 +10,6 @@
// Execute `rustlings hint tests1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[cfg(test)]
mod tests {
#[test]
diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs
index f8024e9..aea5c0e 100644
--- a/exercises/17_tests/tests2.rs
+++ b/exercises/17_tests/tests2.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint tests2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[cfg(test)]
mod tests {
#[test]
diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs
index 4013e38..d815e05 100644
--- a/exercises/17_tests/tests3.rs
+++ b/exercises/17_tests/tests3.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint tests3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub fn is_even(num: i32) -> bool {
num % 2 == 0
}
diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs
index 935d0db..0972a5b 100644
--- a/exercises/17_tests/tests4.rs
+++ b/exercises/17_tests/tests4.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint tests4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
struct Rectangle {
width: i32,
height: i32
diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs
index 31076bb..7ec7da2 100644
--- a/exercises/18_iterators/iterators1.rs
+++ b/exercises/18_iterators/iterators1.rs
@@ -9,8 +9,6 @@
// Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[test]
fn main() {
let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"];
diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs
index dda82a0..4ca7742 100644
--- a/exercises/18_iterators/iterators2.rs
+++ b/exercises/18_iterators/iterators2.rs
@@ -6,8 +6,6 @@
// Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
// Step 1.
// Complete the `capitalize_first` function.
// "hello" -> "Hello"
diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs
index 29fa23a..f7da049 100644
--- a/exercises/18_iterators/iterators3.rs
+++ b/exercises/18_iterators/iterators3.rs
@@ -9,8 +9,6 @@
// Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[derive(Debug, PartialEq, Eq)]
pub enum DivisionError {
NotDivisible(NotDivisibleError),
diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs
index 3c0724e..af3958c 100644
--- a/exercises/18_iterators/iterators4.rs
+++ b/exercises/18_iterators/iterators4.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
pub fn factorial(num: u64) -> u64 {
// Complete this function to return the factorial of num
// Do not use:
diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs
index a062ee4..ceec536 100644
--- a/exercises/18_iterators/iterators5.rs
+++ b/exercises/18_iterators/iterators5.rs
@@ -11,8 +11,6 @@
// Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Eq)]
diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs
index 3526ddc..0647eea 100644
--- a/exercises/19_smart_pointers/arc1.rs
+++ b/exercises/19_smart_pointers/arc1.rs
@@ -21,8 +21,6 @@
//
// Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
#![forbid(unused_imports)] // Do not change this, (or the next) line.
use std::sync::Arc;
use std::thread;
diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs
index 513e7da..2abc024 100644
--- a/exercises/19_smart_pointers/box1.rs
+++ b/exercises/19_smart_pointers/box1.rs
@@ -18,8 +18,6 @@
//
// Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
#[derive(PartialEq, Debug)]
pub enum List {
Cons(i32, List),
diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs
index fcd3e0b..b24591b 100644
--- a/exercises/19_smart_pointers/cow1.rs
+++ b/exercises/19_smart_pointers/cow1.rs
@@ -12,8 +12,6 @@
//
// Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
use std::borrow::Cow;
fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> {
diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs
index 1b90346..e96e625 100644
--- a/exercises/19_smart_pointers/rc1.rs
+++ b/exercises/19_smart_pointers/rc1.rs
@@ -10,8 +10,6 @@
//
// Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
use std::rc::Rc;
#[derive(Debug)]
diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs
index 80b6def..be1301d 100644
--- a/exercises/20_threads/threads1.rs
+++ b/exercises/20_threads/threads1.rs
@@ -8,8 +8,6 @@
// Execute `rustlings hint threads1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::thread;
use std::time::{Duration, Instant};
diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs
index 60d6824..13cb840 100644
--- a/exercises/20_threads/threads2.rs
+++ b/exercises/20_threads/threads2.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint threads2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::sync::Arc;
use std::thread;
use std::time::Duration;
diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs
index acb97b4..35b914a 100644
--- a/exercises/20_threads/threads3.rs
+++ b/exercises/20_threads/threads3.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint threads3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs
index 678de6e..65986db 100644
--- a/exercises/21_macros/macros1.rs
+++ b/exercises/21_macros/macros1.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint macros1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
macro_rules! my_macro {
() => {
println!("Check out my macro!");
diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs
index 788fc16..b7c37fd 100644
--- a/exercises/21_macros/macros2.rs
+++ b/exercises/21_macros/macros2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint macros2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
my_macro!();
}
diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs
index b795c14..92a1922 100644
--- a/exercises/21_macros/macros3.rs
+++ b/exercises/21_macros/macros3.rs
@@ -5,8 +5,6 @@
// Execute `rustlings hint macros3` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
mod macros {
macro_rules! my_macro {
() => {
diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs
index 71b45a0..83a6e44 100644
--- a/exercises/21_macros/macros4.rs
+++ b/exercises/21_macros/macros4.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint macros4` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
#[rustfmt::skip]
macro_rules! my_macro {
() => {
diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs
index e0c6ce7c4..1e0f42e 100644
--- a/exercises/22_clippy/clippy1.rs
+++ b/exercises/22_clippy/clippy1.rs
@@ -9,8 +9,6 @@
// Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
use std::f32;
fn main() {
diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs
index 9b87a0b..37ac089 100644
--- a/exercises/22_clippy/clippy2.rs
+++ b/exercises/22_clippy/clippy2.rs
@@ -3,8 +3,6 @@
// Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn main() {
let mut res = 42;
let option = Some(12);
diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs
index 5a95f5b..6a6a36b 100644
--- a/exercises/22_clippy/clippy3.rs
+++ b/exercises/22_clippy/clippy3.rs
@@ -3,8 +3,6 @@
// Here's a couple more easy Clippy fixes, so you can see its utility.
// No hints.
-// I AM NOT DONE
-
#[allow(unused_variables, unused_assignments)]
fn main() {
let my_option: Option<()> = None;
diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs
index 2ba9e3f..cd2c93b 100644
--- a/exercises/23_conversions/as_ref_mut.rs
+++ b/exercises/23_conversions/as_ref_mut.rs
@@ -7,8 +7,6 @@
// Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
// Obtain the number of bytes (not characters) in the given argument.
// TODO: Add the AsRef trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize {
diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs
index 11787c3..d2a1609 100644
--- a/exercises/23_conversions/from_into.rs
+++ b/exercises/23_conversions/from_into.rs
@@ -41,8 +41,6 @@ impl Default for Person {
// If while parsing the age, something goes wrong, then return the default of
// Person Otherwise, then return an instantiated Person object with the results
-// I AM NOT DONE
-
impl From<&str> for Person {
fn from(s: &str) -> Person {}
}
diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs
index e209347..ed91ca5 100644
--- a/exercises/23_conversions/from_str.rs
+++ b/exercises/23_conversions/from_str.rs
@@ -31,8 +31,6 @@ enum ParsePersonError {
ParseInt(ParseIntError),
}
-// I AM NOT DONE
-
// Steps:
// 1. If the length of the provided string is 0, an error should be returned
// 2. Split the given string on the commas present in it
diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs
index 32d6ef3..2316655 100644
--- a/exercises/23_conversions/try_from_into.rs
+++ b/exercises/23_conversions/try_from_into.rs
@@ -27,8 +27,6 @@ enum IntoColorError {
IntConversion,
}
-// I AM NOT DONE
-
// Your task is to complete this implementation and return an Ok result of inner
// type Color. You need to create an implementation for a tuple of three
// integers, an array of three integers, and a slice of integers.
diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs
index 414cef3..9f617ec 100644
--- a/exercises/23_conversions/using_as.rs
+++ b/exercises/23_conversions/using_as.rs
@@ -10,8 +10,6 @@
// Execute `rustlings hint using_as` or use the `hint` watch subcommand for a
// hint.
-// I AM NOT DONE
-
fn average(values: &[f64]) -> f64 {
let total = values.iter().sum::<f64>();
total / values.len()
diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs
index 4ee5ada..b9e71f5 100644
--- a/exercises/quiz1.rs
+++ b/exercises/quiz1.rs
@@ -13,8 +13,6 @@
//
// No hints this time ;)
-// I AM NOT DONE
-
// Put your function here!
// fn calculate_price_of_apples {
diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs
index 29925ca..8ace3fe 100644
--- a/exercises/quiz2.rs
+++ b/exercises/quiz2.rs
@@ -20,8 +20,6 @@
//
// No hints this time!
-// I AM NOT DONE
-
pub enum Command {
Uppercase,
Trim,
diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs
index 3b01d31..24f7082 100644
--- a/exercises/quiz3.rs
+++ b/exercises/quiz3.rs
@@ -16,8 +16,6 @@
//
// Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint.
-// I AM NOT DONE
-
pub struct ReportCard {
pub grade: f32,
pub student_name: String,
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 1523898..0000000
--- a/flake.lock
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "nodes": {
- "flake-compat": {
- "flake": false,
- "locked": {
- "lastModified": 1673956053,
- "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
- "flake-utils": {
- "inputs": {
- "systems": "systems"
- },
- "locked": {
- "lastModified": 1692799911,
- "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
- "owner": "numtide",
- "repo": "flake-utils",
- "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "flake-utils",
- "type": "github"
- }
- },
- "nixpkgs": {
- "locked": {
- "lastModified": 1694183432,
- "narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=",
- "owner": "nixos",
- "repo": "nixpkgs",
- "rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b",
- "type": "github"
- },
- "original": {
- "owner": "nixos",
- "ref": "nixos-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "flake-compat": "flake-compat",
- "flake-utils": "flake-utils",
- "nixpkgs": "nixpkgs"
- }
- },
- "systems": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 152d38e..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- description = "Small exercises to get you used to reading and writing Rust code";
-
- inputs = {
- flake-compat = {
- url = "github:edolstra/flake-compat";
- flake = false;
- };
- flake-utils.url = "github:numtide/flake-utils";
- nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
- };
-
- outputs = { self, flake-utils, nixpkgs, ... }:
- flake-utils.lib.eachDefaultSystem (system:
- let
- pkgs = nixpkgs.legacyPackages.${system};
-
- cargoBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [
- darwin.apple_sdk.frameworks.CoreServices
- ];
-
- rustlings =
- pkgs.rustPlatform.buildRustPackage {
- name = "rustlings";
- version = "5.6.1";
-
- buildInputs = cargoBuildInputs;
- nativeBuildInputs = [pkgs.git];
-
- src = with pkgs.lib; cleanSourceWith {
- src = self;
- # a function that returns a bool determining if the path should be included in the cleaned source
- filter = path: type:
- let
- # filename
- baseName = builtins.baseNameOf (toString path);
- # path from root directory
- path' = builtins.replaceStrings [ "${self}/" ] [ "" ] path;
- # checks if path is in the directory
- inDirectory = directory: hasPrefix directory path';
- in
- inDirectory "src" ||
- inDirectory "tests" ||
- hasPrefix "Cargo" baseName ||
- baseName == "info.toml";
- };
-
- cargoLock.lockFile = ./Cargo.lock;
- };
- in
- {
- devShell = pkgs.mkShell {
- RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
-
- buildInputs = with pkgs; [
- cargo
- rustc
- rust-analyzer
- rustlings
- rustfmt
- clippy
- ] ++ cargoBuildInputs;
- };
- apps = let
- rustlings-app = {
- type = "app";
- program = "${rustlings}/bin/rustlings";
- };
- in {
- default = rustlings-app;
- rustlings = rustlings-app;
- };
- packages = {
- inherit rustlings;
- default = rustlings;
- };
- });
-}
diff --git a/gen-dev-cargo-toml/Cargo.toml b/gen-dev-cargo-toml/Cargo.toml
new file mode 100644
index 0000000..8922ae8
--- /dev/null
+++ b/gen-dev-cargo-toml/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "gen-dev-cargo-toml"
+publish = false
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true
+toml_edit.workspace = true
diff --git a/src/bin/gen-dev-cargo-toml.rs b/gen-dev-cargo-toml/src/main.rs
index ff8f31d..43b4ebd 100644
--- a/src/bin/gen-dev-cargo-toml.rs
+++ b/gen-dev-cargo-toml/src/main.rs
@@ -1,5 +1,5 @@
// Generates `dev/Cargo.toml` such that it is synced with `info.toml`.
-// `dev/Cargo.toml` is a hack to allow using `cargo r` to test `rustlings`
+// `dev/Cargo.toml` is a hack to allow using `cargo run` to test `rustlings`
// during development.
use anyhow::{bail, Context, Result};
@@ -10,18 +10,18 @@ use std::{
};
#[derive(Deserialize)]
-struct Exercise {
+struct ExerciseInfo {
name: String,
- path: String,
+ dir: Option<String>,
}
#[derive(Deserialize)]
-struct InfoToml {
- exercises: Vec<Exercise>,
+struct InfoFile {
+ exercises: Vec<ExerciseInfo>,
}
fn main() -> Result<()> {
- let exercises = toml_edit::de::from_str::<InfoToml>(
+ let exercise_infos = toml_edit::de::from_str::<InfoFile>(
&fs::read_to_string("info.toml").context("Failed to read `info.toml`")?,
)
.context("Failed to deserialize `info.toml`")?
@@ -30,25 +30,29 @@ fn main() -> Result<()> {
let mut buf = Vec::with_capacity(1 << 14);
buf.extend_from_slice(
- b"# This file is a hack to allow using `cargo r` to test `rustlings` during development.
-# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`.
+ b"# This file is a hack to allow using `cargo run` to test `rustlings` during development.
+# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`.
bin = [\n",
);
- for exercise in exercises {
+ for exercise_info in exercise_infos {
buf.extend_from_slice(b" { name = \"");
- buf.extend_from_slice(exercise.name.as_bytes());
- buf.extend_from_slice(b"\", path = \"../");
- buf.extend_from_slice(exercise.path.as_bytes());
- buf.extend_from_slice(b"\" },\n");
+ buf.extend_from_slice(exercise_info.name.as_bytes());
+ buf.extend_from_slice(b"\", path = \"../exercises/");
+ if let Some(dir) = &exercise_info.dir {
+ buf.extend_from_slice(dir.as_bytes());
+ buf.push(b'/');
+ }
+ buf.extend_from_slice(exercise_info.name.as_bytes());
+ buf.extend_from_slice(b".rs\" },\n");
}
buf.extend_from_slice(
br#"]
[package]
-name = "rustlings"
+name = "rustlings-dev"
edition = "2021"
publish = false
"#,
diff --git a/info.toml b/info.toml
index 36629b3..fa90ad7 100644
--- a/info.toml
+++ b/info.toml
@@ -1,17 +1,52 @@
+welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are
+going to teach you a lot of things about Rust, but before we can get
+started, here's a couple of notes about how Rustlings operates:
+
+1. The central concept behind Rustlings is that you solve exercises. These
+ exercises usually have some sort of syntax error in them, which will cause
+ them to fail compilation or testing. Sometimes there's a logic error instead
+ of a syntax error. No matter what error, it's your job to find it and fix it!
+ You'll know when you fixed it because then, the exercise will compile and
+ Rustlings will be able to move on to the next exercise.
+2. If you run Rustlings in watch mode (which we recommend), it'll automatically
+ start with the first exercise. Don't get confused by an error message popping
+ up as soon as you run Rustlings! This is part of the exercise that you're
+ supposed to solve, so open the exercise file in an editor and start your
+ detective work!
+3. If you're stuck on an exercise, there is a helpful hint you can view by typing
+ 'hint' (in watch mode), or running `rustlings hint exercise_name`.
+4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
+ (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
+ and sometimes, other learners do too so you can help each other out!
+
+Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
+Make sure to have your editor open in the `rustlings` directory!
+"""
+
+final_message = """We hope you enjoyed learning about the various aspects of Rust!
+If you noticed any issues, please don't hesitate to report them to our repo.
+You can also contribute your own exercises to help the greater community!
+
+Before reporting an issue or contributing, please read our guidelines:
+https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md
+"""
+
# INTRO
+# TODO: Update exercise
[[exercises]]
name = "intro1"
-path = "exercises/00_intro/intro1.rs"
-mode = "compile"
+dir = "00_intro"
+mode = "run"
+# TODO: Fix hint
hint = """
Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file
to move on to the next exercise."""
[[exercises]]
name = "intro2"
-path = "exercises/00_intro/intro2.rs"
-mode = "compile"
+dir = "00_intro"
+mode = "run"
hint = """
The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative."""
@@ -19,16 +54,16 @@ The compiler is informing us that we've got the name of the print macro wrong, a
[[exercises]]
name = "variables1"
-path = "exercises/01_variables/variables1.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
The declaration in the first line in the main function is missing a keyword
that is needed in Rust to create a new variable binding."""
[[exercises]]
name = "variables2"
-path = "exercises/01_variables/variables2.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
The compiler message is saying that Rust cannot infer the type that the
variable binding `x` has with what is given here.
@@ -46,8 +81,8 @@ What if `x` is the same type as `10`? What if it's a different type?"""
[[exercises]]
name = "variables3"
-path = "exercises/01_variables/variables3.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
Oops! In this exercise, we have a variable binding that we've created on in the
first line in the `main` function, and we're trying to use it in the next line,
@@ -60,8 +95,8 @@ programming language -- thankfully the Rust compiler has caught this for us!"""
[[exercises]]
name = "variables4"
-path = "exercises/01_variables/variables4.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
In Rust, variable bindings are immutable by default. But here we're trying
to reassign a different value to `x`! There's a keyword we can use to make
@@ -69,8 +104,8 @@ a variable binding mutable instead."""
[[exercises]]
name = "variables5"
-path = "exercises/01_variables/variables5.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
In `variables4` we already learned how to make an immutable variable mutable
using a special keyword. Unfortunately this doesn't help us much in this
@@ -87,8 +122,8 @@ Try to solve this exercise afterwards using this technique."""
[[exercises]]
name = "variables6"
-path = "exercises/01_variables/variables6.rs"
-mode = "compile"
+dir = "01_variables"
+mode = "run"
hint = """
We know about variables and mutability, but there is another important type of
variable available: constants.
@@ -107,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants
[[exercises]]
name = "functions1"
-path = "exercises/02_functions/functions1.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This main function is calling a function that it expects to exist, but the
function doesn't exist. It expects this function to have the name `call_me`.
@@ -117,28 +152,24 @@ Sounds a lot like `main`, doesn't it?"""
[[exercises]]
name = "functions2"
-path = "exercises/02_functions/functions2.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
Rust requires that all parts of a function's signature have type annotations,
but `call_me` is missing the type annotation of `num`."""
[[exercises]]
name = "functions3"
-path = "exercises/02_functions/functions3.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This time, the function *declaration* is okay, but there's something wrong
-with the place where we're calling the function.
-
-As a reminder, you can freely play around with different solutions in Rustlings!
-Watch mode will only jump to the next exercise if you remove the `I AM NOT
-DONE` comment."""
+with the place where we're calling the function."""
[[exercises]]
name = "functions4"
-path = "exercises/02_functions/functions4.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
The error message points to the function `sale_price` and says it expects a type
after the `->`. This is where the function's return type should be -- take a
@@ -149,8 +180,8 @@ for the inputs of the functions here, since the original prices shouldn't be neg
[[exercises]]
name = "functions5"
-path = "exercises/02_functions/functions5.rs"
-mode = "compile"
+dir = "02_functions"
+mode = "run"
hint = """
This is a really common error that can be fixed by removing one character.
It happens because Rust distinguishes between expressions and statements:
@@ -168,7 +199,7 @@ They are not the same. There are two solutions:
[[exercises]]
name = "if1"
-path = "exercises/03_if/if1.rs"
+dir = "03_if"
mode = "test"
hint = """
It's possible to do this in one line if you would like!
@@ -184,7 +215,7 @@ Remember in Rust that:
[[exercises]]
name = "if2"
-path = "exercises/03_if/if2.rs"
+dir = "03_if"
mode = "test"
hint = """
For that first compiler error, it's important in Rust that each conditional
@@ -193,7 +224,7 @@ conditions checking different input values."""
[[exercises]]
name = "if3"
-path = "exercises/03_if/if3.rs"
+dir = "03_if"
mode = "test"
hint = """
In Rust, every arm of an `if` expression has to return the same type of value.
@@ -203,7 +234,6 @@ Make sure the type is consistent across all arms."""
[[exercises]]
name = "quiz1"
-path = "exercises/quiz1.rs"
mode = "test"
hint = "No hints this time ;)"
@@ -211,20 +241,20 @@ hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types1"
-path = "exercises/04_primitive_types/primitive_types1.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types2"
-path = "exercises/04_primitive_types/primitive_types2.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "primitive_types3"
-path = "exercises/04_primitive_types/primitive_types3.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = """
There's a shorthand to initialize Arrays with a certain size that does not
require you to type in 100 items (but you certainly can if you want!).
@@ -239,7 +269,7 @@ for `a.len() >= 100`?"""
[[exercises]]
name = "primitive_types4"
-path = "exercises/04_primitive_types/primitive_types4.rs"
+dir = "04_primitive_types"
mode = "test"
hint = """
Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section
@@ -254,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html"""
[[exercises]]
name = "primitive_types5"
-path = "exercises/04_primitive_types/primitive_types5.rs"
-mode = "compile"
+dir = "04_primitive_types"
+mode = "run"
hint = """
Take a look at the 'Data Types -> The Tuple Type' section of the book:
https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type
@@ -267,7 +297,7 @@ of the tuple. You can do it!!"""
[[exercises]]
name = "primitive_types6"
-path = "exercises/04_primitive_types/primitive_types6.rs"
+dir = "04_primitive_types"
mode = "test"
hint = """
While you could use a destructuring `let` for the tuple here, try
@@ -280,7 +310,7 @@ Now you have another tool in your toolbox!"""
[[exercises]]
name = "vecs1"
-path = "exercises/05_vecs/vecs1.rs"
+dir = "05_vecs"
mode = "test"
hint = """
In Rust, there are two ways to define a Vector.
@@ -295,7 +325,7 @@ of the Rust book to learn more.
[[exercises]]
name = "vecs2"
-path = "exercises/05_vecs/vecs2.rs"
+dir = "05_vecs"
mode = "test"
hint = """
In the first function we are looping over the Vector and getting a reference to
@@ -318,7 +348,7 @@ What do you think is the more commonly used pattern under Rust developers?
[[exercises]]
name = "move_semantics1"
-path = "exercises/06_move_semantics/move_semantics1.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
So you've got the "cannot borrow immutable local variable `vec` as mutable"
@@ -332,7 +362,7 @@ happens!"""
[[exercises]]
name = "move_semantics2"
-path = "exercises/06_move_semantics/move_semantics2.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
When running this exercise for the first time, you'll notice an error about
@@ -353,7 +383,7 @@ try them all:
[[exercises]]
name = "move_semantics3"
-path = "exercises/06_move_semantics/move_semantics3.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
The difference between this one and the previous ones is that the first line
@@ -363,7 +393,7 @@ an existing binding to be a mutable binding instead of an immutable one :)"""
[[exercises]]
name = "move_semantics4"
-path = "exercises/06_move_semantics/move_semantics4.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
Stop reading whenever you feel like you have enough direction :) Or try
@@ -377,7 +407,7 @@ So the end goal is to:
[[exercises]]
name = "move_semantics5"
-path = "exercises/06_move_semantics/move_semantics5.rs"
+dir = "06_move_semantics"
mode = "test"
hint = """
Carefully reason about the range in which each mutable reference is in
@@ -389,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref
[[exercises]]
name = "move_semantics6"
-path = "exercises/06_move_semantics/move_semantics6.rs"
-mode = "compile"
+dir = "06_move_semantics"
+mode = "run"
hint = """
To find the answer, you can consult the book section "References and Borrowing":
https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html
@@ -410,7 +440,7 @@ Another hint: it has to do with the `&` character."""
[[exercises]]
name = "structs1"
-path = "exercises/07_structs/structs1.rs"
+dir = "07_structs"
mode = "test"
hint = """
Rust has more than one type of struct. Three actually, all variants are used to
@@ -430,7 +460,7 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html"""
[[exercises]]
name = "structs2"
-path = "exercises/07_structs/structs2.rs"
+dir = "07_structs"
mode = "test"
hint = """
Creating instances of structs is easy, all you need to do is assign some values
@@ -442,7 +472,7 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins
[[exercises]]
name = "structs3"
-path = "exercises/07_structs/structs3.rs"
+dir = "07_structs"
mode = "test"
hint = """
For `is_international`: What makes a package international? Seems related to
@@ -458,21 +488,21 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html"""
[[exercises]]
name = "enums1"
-path = "exercises/08_enums/enums1.rs"
-mode = "compile"
+dir = "08_enums"
+mode = "run"
hint = "No hints this time ;)"
[[exercises]]
name = "enums2"
-path = "exercises/08_enums/enums2.rs"
-mode = "compile"
+dir = "08_enums"
+mode = "run"
hint = """
You can create enumerations that have different variants with different types
such as no data, anonymous structs, a single string, tuples, ...etc"""
[[exercises]]
name = "enums3"
-path = "exercises/08_enums/enums3.rs"
+dir = "08_enums"
mode = "test"
hint = """
As a first step, you can define enums to compile this code without errors.
@@ -486,8 +516,8 @@ to get value in the variant."""
[[exercises]]
name = "strings1"
-path = "exercises/09_strings/strings1.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = """
The `current_favorite_color` function is currently returning a string slice
with the `'static` lifetime. We know this because the data of the string lives
@@ -500,8 +530,8 @@ another way that uses the `From` trait."""
[[exercises]]
name = "strings2"
-path = "exercises/09_strings/strings2.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = """
Yes, it would be really easy to fix this by just changing the value bound to
`word` to be a string slice instead of a `String`, wouldn't it?? There is a way
@@ -515,7 +545,7 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion
[[exercises]]
name = "strings3"
-path = "exercises/09_strings/strings3.rs"
+dir = "09_strings"
mode = "test"
hint = """
There's tons of useful standard library functions for strings. Let's try and use some of them:
@@ -526,16 +556,16 @@ the string slice into an owned string, which you can then freely extend."""
[[exercises]]
name = "strings4"
-path = "exercises/09_strings/strings4.rs"
-mode = "compile"
+dir = "09_strings"
+mode = "run"
hint = "No hints this time ;)"
# MODULES
[[exercises]]
name = "modules1"
-path = "exercises/10_modules/modules1.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
Everything is private in Rust by default-- but there's a keyword we can use
to make something public! The compiler error should point to the thing that
@@ -543,8 +573,8 @@ needs to be public."""
[[exercises]]
name = "modules2"
-path = "exercises/10_modules/modules2.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
The delicious_snacks module is trying to present an external interface that is
different than its internal structure (the `fruits` and `veggies` modules and
@@ -555,8 +585,8 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w
[[exercises]]
name = "modules3"
-path = "exercises/10_modules/modules3.rs"
-mode = "compile"
+dir = "10_modules"
+mode = "run"
hint = """
`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a
`use` statement for these two to bring them into scope. You can use nested
@@ -566,7 +596,7 @@ paths or the glob operator to bring these two in using only one line."""
[[exercises]]
name = "hashmaps1"
-path = "exercises/11_hashmaps/hashmaps1.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Hint 1: Take a look at the return type of the function to figure out
@@ -578,7 +608,7 @@ Hint 2: Number of fruits should be at least 5. And you have to put
[[exercises]]
name = "hashmaps2"
-path = "exercises/11_hashmaps/hashmaps2.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this.
@@ -587,7 +617,7 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-
[[exercises]]
name = "hashmaps3"
-path = "exercises/11_hashmaps/hashmaps3.rs"
+dir = "11_hashmaps"
mode = "test"
hint = """
Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert
@@ -605,7 +635,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v
[[exercises]]
name = "quiz2"
-path = "exercises/quiz2.rs"
mode = "test"
hint = "No hints this time ;)"
@@ -613,7 +642,7 @@ hint = "No hints this time ;)"
[[exercises]]
name = "options1"
-path = "exercises/12_options/options1.rs"
+dir = "12_options"
mode = "test"
hint = """
Options can have a `Some` value, with an inner value, or a `None` value,
@@ -625,7 +654,7 @@ it doesn't panic in your face later?"""
[[exercises]]
name = "options2"
-path = "exercises/12_options/options2.rs"
+dir = "12_options"
mode = "test"
hint = """
Check out:
@@ -642,8 +671,8 @@ Also see `Option::flatten`
[[exercises]]
name = "options3"
-path = "exercises/12_options/options3.rs"
-mode = "compile"
+dir = "12_options"
+mode = "run"
hint = """
The compiler says a partial move happened in the `match` statement. How can
this be avoided? The compiler shows the correction needed.
@@ -655,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html"""
[[exercises]]
name = "errors1"
-path = "exercises/13_error_handling/errors1.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
`Ok` and `Err` are the two variants of `Result`, so what the tests are saying
@@ -671,7 +700,7 @@ To make this change, you'll need to:
[[exercises]]
name = "errors2"
-path = "exercises/13_error_handling/errors2.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
One way to handle this is using a `match` statement on
@@ -687,8 +716,8 @@ and give it a try!"""
[[exercises]]
name = "errors3"
-path = "exercises/13_error_handling/errors3.rs"
-mode = "compile"
+dir = "13_error_handling"
+mode = "run"
hint = """
If other functions can return a `Result`, why shouldn't `main`? It's a fairly
common convention to return something like `Result<(), ErrorType>` from your
@@ -699,7 +728,7 @@ positive results."""
[[exercises]]
name = "errors4"
-path = "exercises/13_error_handling/errors4.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
`PositiveNonzeroInteger::new` is always creating a new instance and returning
@@ -711,8 +740,8 @@ everything is... okay :)"""
[[exercises]]
name = "errors5"
-path = "exercises/13_error_handling/errors5.rs"
-mode = "compile"
+dir = "13_error_handling"
+mode = "run"
hint = """
There are two different possible `Result` types produced within `main()`, which
are propagated using `?` operators. How do we declare a return type from
@@ -735,7 +764,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]]
name = "errors6"
-path = "exercises/13_error_handling/errors6.rs"
+dir = "13_error_handling"
mode = "test"
hint = """
This exercise uses a completed version of `PositiveNonzeroInteger` from
@@ -757,8 +786,8 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err"""
[[exercises]]
name = "generics1"
-path = "exercises/14_generics/generics1.rs"
-mode = "compile"
+dir = "14_generics"
+mode = "run"
hint = """
Vectors in Rust make use of generics to create dynamically sized arrays of any
type.
@@ -767,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector."""
[[exercises]]
name = "generics2"
-path = "exercises/14_generics/generics2.rs"
+dir = "14_generics"
mode = "test"
hint = """
Currently we are wrapping only values of type `u32`.
@@ -781,7 +810,7 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html
[[exercises]]
name = "traits1"
-path = "exercises/15_traits/traits1.rs"
+dir = "15_traits"
mode = "test"
hint = """
A discussion about Traits in Rust can be found at:
@@ -790,7 +819,7 @@ https://doc.rust-lang.org/book/ch10-02-traits.html
[[exercises]]
name = "traits2"
-path = "exercises/15_traits/traits2.rs"
+dir = "15_traits"
mode = "test"
hint = """
Notice how the trait takes ownership of `self`, and returns `Self`.
@@ -803,7 +832,7 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html"""
[[exercises]]
name = "traits3"
-path = "exercises/15_traits/traits3.rs"
+dir = "15_traits"
mode = "test"
hint = """
Traits can have a default implementation for functions. Structs that implement
@@ -815,7 +844,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def
[[exercises]]
name = "traits4"
-path = "exercises/15_traits/traits4.rs"
+dir = "15_traits"
mode = "test"
hint = """
Instead of using concrete types as parameters you can use traits. Try replacing
@@ -826,8 +855,8 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra
[[exercises]]
name = "traits5"
-path = "exercises/15_traits/traits5.rs"
-mode = "compile"
+dir = "15_traits"
+mode = "run"
hint = """
To ensure a parameter implements multiple traits use the '+ syntax'. Try
replacing the '??' with 'impl <> + <>'.
@@ -839,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe
[[exercises]]
name = "quiz3"
-path = "exercises/quiz3.rs"
mode = "test"
hint = """
To find the best solution to this challenge you're going to need to think back
@@ -851,16 +879,16 @@ You may also need this: `use std::fmt::Display;`."""
[[exercises]]
name = "lifetimes1"
-path = "exercises/16_lifetimes/lifetimes1.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
Let the compiler guide you. Also take a look at the book if you need help:
https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"""
[[exercises]]
name = "lifetimes2"
-path = "exercises/16_lifetimes/lifetimes2.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
Remember that the generic lifetime `'a` will get the concrete lifetime that is
equal to the smaller of the lifetimes of `x` and `y`.
@@ -873,8 +901,8 @@ inner block:
[[exercises]]
name = "lifetimes3"
-path = "exercises/16_lifetimes/lifetimes3.rs"
-mode = "compile"
+dir = "16_lifetimes"
+mode = "run"
hint = """
If you use a lifetime annotation in a struct's fields, where else does it need
to be added?"""
@@ -883,7 +911,7 @@ to be added?"""
[[exercises]]
name = "tests1"
-path = "exercises/17_tests/tests1.rs"
+dir = "17_tests"
mode = "test"
hint = """
You don't even need to write any code to test -- you can just test values and
@@ -898,7 +926,7 @@ ones pass, and which ones fail :)"""
[[exercises]]
name = "tests2"
-path = "exercises/17_tests/tests2.rs"
+dir = "17_tests"
mode = "test"
hint = """
Like the previous exercise, you don't need to write any code to get this test
@@ -911,7 +939,7 @@ argument comes first and which comes second!"""
[[exercises]]
name = "tests3"
-path = "exercises/17_tests/tests3.rs"
+dir = "17_tests"
mode = "test"
hint = """
You can call a function right where you're passing arguments to `assert!`. So
@@ -922,7 +950,7 @@ what you're doing using `!`, like `assert!(!having_fun())`."""
[[exercises]]
name = "tests4"
-path = "exercises/17_tests/tests4.rs"
+dir = "17_tests"
mode = "test"
hint = """
We expect method `Rectangle::new()` to panic for negative values.
@@ -936,7 +964,7 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa
[[exercises]]
name = "iterators1"
-path = "exercises/18_iterators/iterators1.rs"
+dir = "18_iterators"
mode = "test"
hint = """
Step 1:
@@ -959,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.
[[exercises]]
name = "iterators2"
-path = "exercises/18_iterators/iterators2.rs"
+dir = "18_iterators"
mode = "test"
hint = """
Step 1:
@@ -985,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type."""
[[exercises]]
name = "iterators3"
-path = "exercises/18_iterators/iterators3.rs"
+dir = "18_iterators"
mode = "test"
hint = """
The `divide` function needs to return the correct error when even division is
@@ -1004,7 +1032,7 @@ powerful! It can make the solution to this exercise infinitely easier."""
[[exercises]]
name = "iterators4"
-path = "exercises/18_iterators/iterators4.rs"
+dir = "18_iterators"
mode = "test"
hint = """
In an imperative language, you might write a `for` loop that updates a mutable
@@ -1016,7 +1044,7 @@ Hint 2: Check out the `fold` and `rfold` methods!"""
[[exercises]]
name = "iterators5"
-path = "exercises/18_iterators/iterators5.rs"
+dir = "18_iterators"
mode = "test"
hint = """
The documentation for the `std::iter::Iterator` trait contains numerous methods
@@ -1035,7 +1063,7 @@ a different method that could make your code more compact than using `fold`."""
[[exercises]]
name = "box1"
-path = "exercises/19_smart_pointers/box1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
Step 1:
@@ -1059,7 +1087,7 @@ definition and try other types!
[[exercises]]
name = "rc1"
-path = "exercises/19_smart_pointers/rc1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
This is a straightforward exercise to use the `Rc<T>` type. Each `Planet` has
@@ -1078,8 +1106,8 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html
[[exercises]]
name = "arc1"
-path = "exercises/19_smart_pointers/arc1.rs"
-mode = "compile"
+dir = "19_smart_pointers"
+mode = "run"
hint = """
Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order
to avoid creating a copy of `numbers`, you'll need to create `child_numbers`
@@ -1096,7 +1124,7 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html
[[exercises]]
name = "cow1"
-path = "exercises/19_smart_pointers/cow1.rs"
+dir = "19_smart_pointers"
mode = "test"
hint = """
If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is
@@ -1110,8 +1138,8 @@ on the `Cow` type.
[[exercises]]
name = "threads1"
-path = "exercises/20_threads/threads1.rs"
-mode = "compile"
+dir = "20_threads"
+mode = "run"
hint = """
`JoinHandle` is a struct that is returned from a spawned thread:
https://doc.rust-lang.org/std/thread/fn.spawn.html
@@ -1128,8 +1156,8 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html
[[exercises]]
name = "threads2"
-path = "exercises/20_threads/threads2.rs"
-mode = "compile"
+dir = "20_threads"
+mode = "run"
hint = """
`Arc` is an Atomic Reference Counted pointer that allows safe, shared access
to **immutable** data. But we want to *change* the number of `jobs_completed`
@@ -1150,7 +1178,7 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee
[[exercises]]
name = "threads3"
-path = "exercises/20_threads/threads3.rs"
+dir = "20_threads"
mode = "test"
hint = """
An alternate way to handle concurrency between threads is to use an `mpsc`
@@ -1169,8 +1197,8 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info.
[[exercises]]
name = "macros1"
-path = "exercises/21_macros/macros1.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
When you call a macro, you need to add something special compared to a
regular function call. If you're stuck, take a look at what's inside
@@ -1178,8 +1206,8 @@ regular function call. If you're stuck, take a look at what's inside
[[exercises]]
name = "macros2"
-path = "exercises/21_macros/macros2.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
Macros don't quite play by the same rules as the rest of Rust, in terms of
what's available where.
@@ -1189,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus
[[exercises]]
name = "macros3"
-path = "exercises/21_macros/macros3.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
In order to use a macro outside of its module, you need to do something
special to the module to lift the macro out into its parent.
@@ -1200,8 +1228,8 @@ exported macros, if you've seen any of those around."""
[[exercises]]
name = "macros4"
-path = "exercises/21_macros/macros4.rs"
-mode = "compile"
+dir = "21_macros"
+mode = "run"
hint = """
You only need to add a single character to make this compile.
@@ -1217,7 +1245,7 @@ https://veykril.github.io/tlborm/"""
[[exercises]]
name = "clippy1"
-path = "exercises/22_clippy/clippy1.rs"
+dir = "22_clippy"
mode = "clippy"
hint = """
Rust stores the highest precision version of any long or infinite precision
@@ -1233,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`..."""
[[exercises]]
name = "clippy2"
-path = "exercises/22_clippy/clippy2.rs"
+dir = "22_clippy"
mode = "clippy"
hint = """
`for` loops over `Option` values are more clearly expressed as an `if let`"""
[[exercises]]
name = "clippy3"
-path = "exercises/22_clippy/clippy3.rs"
+dir = "22_clippy"
mode = "clippy"
hint = "No hints this time!"
@@ -1248,7 +1276,7 @@ hint = "No hints this time!"
[[exercises]]
name = "using_as"
-path = "exercises/23_conversions/using_as.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Use the `as` operator to cast one of the operands in the last line of the
@@ -1256,14 +1284,14 @@ Use the `as` operator to cast one of the operands in the last line of the
[[exercises]]
name = "from_into"
-path = "exercises/23_conversions/from_into.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Follow the steps provided right before the `From` implementation"""
[[exercises]]
name = "from_str"
-path = "exercises/23_conversions/from_str.rs"
+dir = "23_conversions"
mode = "test"
hint = """
The implementation of `FromStr` should return an `Ok` with a `Person` object,
@@ -1284,7 +1312,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]]
name = "try_from_into"
-path = "exercises/23_conversions/try_from_into.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Follow the steps provided right before the `TryFrom` implementation.
@@ -1307,7 +1335,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer
[[exercises]]
name = "as_ref_mut"
-path = "exercises/23_conversions/as_ref_mut.rs"
+dir = "23_conversions"
mode = "test"
hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""
diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml
index 0114c8f..79279f5 100644
--- a/rustlings-macros/Cargo.toml
+++ b/rustlings-macros/Cargo.toml
@@ -9,4 +9,4 @@ edition.workspace = true
proc-macro = true
[dependencies]
-quote = "1.0.35"
+quote = "1.0.36"
diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs
index 598b5c3..d8da666 100644
--- a/rustlings-macros/src/lib.rs
+++ b/rustlings-macros/src/lib.rs
@@ -75,7 +75,6 @@ pub fn include_files(_: TokenStream) -> TokenStream {
quote! {
EmbeddedFiles {
- info_toml_content: ::std::include_str!("../info.toml"),
exercises_dir: ExercisesDir {
readme: EmbeddedFile {
path: "exercises/README.md",
diff --git a/shell.nix b/shell.nix
deleted file mode 100644
index fa2a56c..0000000
--- a/shell.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
-in fetchTarball {
- url =
- "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
- sha256 = lock.nodes.flake-compat.locked.narHash;
-}) { src = ./.; }).shellNix
diff --git a/src/app_state.rs b/src/app_state.rs
new file mode 100644
index 0000000..432a9a2
--- /dev/null
+++ b/src/app_state.rs
@@ -0,0 +1,277 @@
+use anyhow::{bail, Context, Result};
+use crossterm::{
+ style::Stylize,
+ terminal::{Clear, ClearType},
+ ExecutableCommand,
+};
+use std::{
+ fs::{self, File},
+ io::{Read, StdoutLock, Write},
+};
+
+use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE};
+
+const STATE_FILE_NAME: &str = ".rustlings-state.txt";
+const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
+
+#[must_use]
+pub enum ExercisesProgress {
+ AllDone,
+ Pending,
+}
+
+pub enum StateFileStatus {
+ Read,
+ NotRead,
+}
+
+pub struct AppState {
+ current_exercise_ind: usize,
+ exercises: Vec<Exercise>,
+ n_done: u16,
+ final_message: String,
+ file_buf: Vec<u8>,
+}
+
+impl AppState {
+ fn update_from_file(&mut self) -> StateFileStatus {
+ self.file_buf.clear();
+ self.n_done = 0;
+
+ if File::open(STATE_FILE_NAME)
+ .and_then(|mut file| file.read_to_end(&mut self.file_buf))
+ .is_err()
+ {
+ return StateFileStatus::NotRead;
+ }
+
+ // See `Self::write` for more information about the file format.
+ let mut lines = self.file_buf.split(|c| *c == b'\n');
+ let Some(current_exercise_name) = lines.next() else {
+ return StateFileStatus::NotRead;
+ };
+
+ if current_exercise_name.is_empty() || lines.next().is_none() {
+ return StateFileStatus::NotRead;
+ }
+
+ let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len());
+
+ for done_exerise_name in lines {
+ if done_exerise_name.is_empty() {
+ break;
+ }
+ done_exercises.insert(done_exerise_name);
+ }
+
+ for (ind, exercise) in self.exercises.iter_mut().enumerate() {
+ if done_exercises.contains(exercise.name.as_bytes()) {
+ exercise.done = true;
+ self.n_done += 1;
+ }
+
+ if exercise.name.as_bytes() == current_exercise_name {
+ self.current_exercise_ind = ind;
+ }
+ }
+
+ StateFileStatus::Read
+ }
+
+ pub fn new(
+ exercise_infos: Vec<ExerciseInfo>,
+ final_message: String,
+ ) -> (Self, StateFileStatus) {
+ let exercises = exercise_infos
+ .into_iter()
+ .map(|mut 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();
+
+ exercise_info.name.shrink_to_fit();
+ let name = exercise_info.name.leak();
+
+ let hint = exercise_info.hint.trim().to_owned();
+
+ Exercise {
+ name,
+ path,
+ mode: exercise_info.mode,
+ hint,
+ done: false,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let mut slf = Self {
+ current_exercise_ind: 0,
+ exercises,
+ n_done: 0,
+ final_message,
+ file_buf: Vec::with_capacity(2048),
+ };
+
+ let state_file_status = slf.update_from_file();
+
+ (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 {
+ self.n_done
+ }
+
+ #[inline]
+ pub fn current_exercise(&self) -> &Exercise {
+ &self.exercises[self.current_exercise_ind]
+ }
+
+ pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> {
+ if ind >= self.exercises.len() {
+ bail!(BAD_INDEX_ERR);
+ }
+
+ self.current_exercise_ind = ind;
+
+ self.write()
+ }
+
+ pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
+ // O(N) is fine since this method is used only once until the program exits.
+ // Building a hashmap would have more overhead.
+ self.current_exercise_ind = self
+ .exercises
+ .iter()
+ .position(|exercise| exercise.name == name)
+ .with_context(|| format!("No exercise found for '{name}'!"))?;
+
+ self.write()
+ }
+
+ pub fn set_pending(&mut self, ind: usize) -> Result<()> {
+ let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?;
+
+ if exercise.done {
+ exercise.done = false;
+ self.n_done -= 1;
+ self.write()?;
+ }
+
+ Ok(())
+ }
+
+ fn next_pending_exercise_ind(&self) -> Option<usize> {
+ if self.current_exercise_ind == self.exercises.len() - 1 {
+ // The last exercise is done.
+ // Search for exercises not done from the start.
+ return self.exercises[..self.current_exercise_ind]
+ .iter()
+ .position(|exercise| !exercise.done);
+ }
+
+ // The done exercise isn't the last one.
+ // Search for a pending exercise after the current one and then from the start.
+ match self.exercises[self.current_exercise_ind + 1..]
+ .iter()
+ .position(|exercise| !exercise.done)
+ {
+ Some(ind) => Some(self.current_exercise_ind + 1 + ind),
+ None => self.exercises[..self.current_exercise_ind]
+ .iter()
+ .position(|exercise| !exercise.done),
+ }
+ }
+
+ pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
+ let exercise = &mut self.exercises[self.current_exercise_ind];
+ if !exercise.done {
+ exercise.done = true;
+ self.n_done += 1;
+ }
+
+ let Some(ind) = self.next_pending_exercise_ind() else {
+ writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
+
+ for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
+ writer.write_fmt(format_args!("Running {exercise} ... "))?;
+ writer.flush()?;
+
+ if !exercise.run()?.status.success() {
+ writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
+
+ self.current_exercise_ind = exercise_ind;
+
+ // No check if the exercise is done before setting it to pending
+ // because no pending exercise was found.
+ self.exercises[exercise_ind].done = false;
+ self.n_done -= 1;
+
+ self.write()?;
+
+ return Ok(ExercisesProgress::Pending);
+ }
+
+ writer.write_fmt(format_args!("{}\n", "ok".green()))?;
+ }
+
+ writer.execute(Clear(ClearType::All))?;
+ writer.write_all(FENISH_LINE.as_bytes())?;
+
+ let final_message = self.final_message.trim();
+ if !final_message.is_empty() {
+ writer.write_all(self.final_message.as_bytes())?;
+ writer.write_all(b"\n")?;
+ }
+
+ return Ok(ExercisesProgress::AllDone);
+ };
+
+ self.set_current_exercise_ind(ind)?;
+
+ Ok(ExercisesProgress::Pending)
+ }
+
+ // Write the state file.
+ // The file's format is very simple:
+ // - The first line is the name of the current exercise. It must end with `\n` even if there
+ // are no done exercises.
+ // - The second line is an empty line.
+ // - All remaining lines are the names of done exercises.
+ fn write(&mut self) -> Result<()> {
+ self.file_buf.clear();
+
+ self.file_buf
+ .extend_from_slice(self.current_exercise().name.as_bytes());
+ self.file_buf.push(b'\n');
+
+ for exercise in &self.exercises {
+ if exercise.done {
+ self.file_buf.push(b'\n');
+ self.file_buf.extend_from_slice(exercise.name.as_bytes());
+ }
+ }
+
+ fs::write(STATE_FILE_NAME, &self.file_buf)
+ .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
+
+ Ok(())
+ }
+}
+
+const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
+All exercises seem to be done.
+Recompiling and running all exercises to make sure that all of them are actually done.
+
+";
diff --git a/src/embedded.rs b/src/embedded.rs
index 56b4b61..866b12b 100644
--- a/src/embedded.rs
+++ b/src/embedded.rs
@@ -65,7 +65,6 @@ struct ExercisesDir {
}
pub struct EmbeddedFiles {
- pub info_toml_content: &'static str,
exercises_dir: ExercisesDir,
}
@@ -92,7 +91,12 @@ impl EmbeddedFiles {
Ok(())
}
- pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> {
+ pub fn write_exercise_to_disk<P>(&self, path: P, strategy: WriteStrategy) -> io::Result<()>
+ where
+ P: AsRef<Path>,
+ {
+ let path = path.as_ref();
+
if let Some(file) = self
.exercises_dir
.files
diff --git a/src/exercise.rs b/src/exercise.rs
index 450acf4..2ec8d97 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,92 +1,47 @@
use anyhow::{Context, Result};
-use serde::Deserialize;
-use std::fmt::{self, Debug, Display, Formatter};
-use std::fs::{self, File};
-use std::io::{self, BufRead, BufReader};
-use std::path::PathBuf;
-use std::process::{exit, Command, Output};
-use std::{array, mem};
-use winnow::ascii::{space0, Caseless};
-use winnow::combinator::opt;
-use winnow::Parser;
-
-use crate::embedded::EMBEDDED_FILES;
-
-// The number of context lines above and below a highlighted line.
-const CONTEXT: usize = 2;
-
-// Check if the line contains the "I AM NOT DONE" comment.
-fn contains_not_done_comment(input: &str) -> bool {
- (
- space0::<_, ()>,
- "//",
- opt('/'),
- space0,
- Caseless("I AM NOT DONE"),
- )
- .parse_next(&mut &*input)
- .is_ok()
-}
-
-// The mode of the exercise.
-#[derive(Deserialize, Copy, Clone)]
-#[serde(rename_all = "lowercase")]
-pub enum Mode {
- // The exercise should be compiled as a binary
- Compile,
- // The exercise should be compiled as a test harness
- Test,
- // The exercise should be linted with clippy
- Clippy,
+use crossterm::style::{style, StyledContent, Stylize};
+use std::{
+ fmt::{self, Display, Formatter},
+ fs,
+ process::{Command, Output},
+};
+
+use crate::{
+ embedded::{WriteStrategy, EMBEDDED_FILES},
+ info_file::Mode,
+};
+
+pub struct TerminalFileLink<'a> {
+ path: &'a str,
}
-#[derive(Deserialize)]
-pub struct ExerciseList {
- pub exercises: Vec<Exercise>,
-}
-
-impl ExerciseList {
- pub fn parse() -> Result<Self> {
- // Read a local `info.toml` if it exists.
- // Mainly to let the tests work for now.
- if let Ok(file_content) = fs::read_to_string("info.toml") {
- toml_edit::de::from_str(&file_content)
+impl<'a> Display for TerminalFileLink<'a> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if let Ok(Some(canonical_path)) = fs::canonicalize(self.path)
+ .as_deref()
+ .map(|path| path.to_str())
+ {
+ write!(
+ f,
+ "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
+ canonical_path, self.path,
+ )
} else {
- toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content)
+ write!(f, "{}", self.path,)
}
- .context("Failed to parse `info.toml`")
}
}
-// Deserialized from the `info.toml` file.
-#[derive(Deserialize)]
pub struct Exercise {
- // Name of the exercise
- pub name: String,
- // The path to the file containing the exercise's source code
- pub path: PathBuf,
+ // Exercise's unique name
+ pub name: &'static str,
+ // Exercise's path
+ pub path: &'static str,
// The mode of the exercise
pub mode: Mode,
// The hint text associated with the exercise
pub hint: String,
-}
-
-// The state of an Exercise.
-#[derive(PartialEq, Eq, Debug)]
-pub enum State {
- Done,
- Pending(Vec<ContextLine>),
-}
-
-// The context information of a pending exercise.
-#[derive(PartialEq, Eq, Debug)]
-pub struct ContextLine {
- // The source code line
- pub line: String,
- // The line number
- pub number: usize,
- // Whether this is important and should be highlighted
- pub important: bool,
+ pub done: bool,
}
impl Exercise {
@@ -105,7 +60,7 @@ impl Exercise {
.arg("always")
.arg("-q")
.arg("--bin")
- .arg(&self.name)
+ .arg(self.name)
.args(args)
.output()
.context("Failed to run Cargo")
@@ -113,8 +68,8 @@ impl Exercise {
pub fn run(&self) -> Result<Output> {
match self.mode {
- Mode::Compile => self.cargo_cmd("run", &[]),
- Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]),
+ Mode::Run => self.cargo_cmd("run", &[]),
+ Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]),
Mode::Clippy => self.cargo_cmd(
"clippy",
&["--", "-D", "warnings", "-D", "clippy::float_cmp"],
@@ -122,107 +77,16 @@ impl Exercise {
}
}
- pub fn state(&self) -> Result<State> {
- let source_file = File::open(&self.path)
- .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?;
- let mut source_reader = BufReader::new(source_file);
-
- // Read the next line into `buf` without the newline at the end.
- let mut read_line = |buf: &mut String| -> io::Result<_> {
- let n = source_reader.read_line(buf)?;
- if buf.ends_with('\n') {
- buf.pop();
- if buf.ends_with('\r') {
- buf.pop();
- }
- }
- Ok(n)
- };
-
- let mut current_line_number: usize = 1;
- // Keep the last `CONTEXT` lines while iterating over the file lines.
- let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256));
- let mut line = String::with_capacity(256);
-
- loop {
- let n = read_line(&mut line).unwrap_or_else(|e| {
- println!(
- "Failed to read the exercise file {}: {e}",
- self.path.display(),
- );
- exit(1);
- });
-
- // Reached the end of the file and didn't find the comment.
- if n == 0 {
- return Ok(State::Done);
- }
-
- if contains_not_done_comment(&line) {
- let mut context = Vec::with_capacity(2 * CONTEXT + 1);
- // Previous lines.
- for (ind, prev_line) in prev_lines
- .into_iter()
- .take(current_line_number - 1)
- .enumerate()
- .rev()
- {
- context.push(ContextLine {
- line: prev_line,
- number: current_line_number - 1 - ind,
- important: false,
- });
- }
-
- // Current line.
- context.push(ContextLine {
- line,
- number: current_line_number,
- important: true,
- });
-
- // Next lines.
- for ind in 0..CONTEXT {
- let mut next_line = String::with_capacity(256);
- let Ok(n) = read_line(&mut next_line) else {
- // If an error occurs, just ignore the next lines.
- break;
- };
-
- // Reached the end of the file.
- if n == 0 {
- break;
- }
-
- context.push(ContextLine {
- line: next_line,
- number: current_line_number + 1 + ind,
- important: false,
- });
- }
-
- return Ok(State::Pending(context));
- }
-
- current_line_number += 1;
- // Add the current line as a previous line and shift the older lines by one.
- for prev_line in &mut prev_lines {
- mem::swap(&mut line, prev_line);
- }
- // The current line now contains the oldest previous line.
- // Recycle it for reading the next line.
- line.clear();
- }
+ pub fn reset(&self) -> Result<()> {
+ EMBEDDED_FILES
+ .write_exercise_to_disk(self.path, WriteStrategy::Overwrite)
+ .with_context(|| format!("Failed to reset the exercise {self}"))
}
- // Check that the exercise looks to be solved using self.state()
- // This is not the best way to check since
- // the user can just remove the "I AM NOT DONE" string from the file
- // without actually having solved anything.
- // The only other way to truly check this would to compile and run
- // the exercise; which would be both costly and counterintuitive
- pub fn looks_done(&self) -> Result<bool> {
- self.state().map(|state| state == State::Done)
+ pub fn terminal_link(&self) -> StyledContent<TerminalFileLink<'_>> {
+ style(TerminalFileLink { path: self.path })
+ .underlined()
+ .blue()
}
}
@@ -231,77 +95,3 @@ impl Display for Exercise {
self.path.fmt(f)
}
}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_pending_state() {
- let exercise = Exercise {
- name: "pending_exercise".into(),
- path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"),
- mode: Mode::Compile,
- hint: String::new(),
- };
-
- let state = exercise.state();
- let expected = vec![
- ContextLine {
- line: "// fake_exercise".to_string(),
- number: 1,
- important: false,
- },
- ContextLine {
- line: "".to_string(),
- number: 2,
- important: false,
- },
- ContextLine {
- line: "// I AM NOT DONE".to_string(),
- number: 3,
- important: true,
- },
- ContextLine {
- line: "".to_string(),
- number: 4,
- important: false,
- },
- ContextLine {
- line: "fn main() {".to_string(),
- number: 5,
- important: false,
- },
- ];
-
- assert_eq!(state.unwrap(), State::Pending(expected));
- }
-
- #[test]
- fn test_finished_exercise() {
- let exercise = Exercise {
- name: "finished_exercise".into(),
- path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"),
- mode: Mode::Compile,
- hint: String::new(),
- };
-
- assert_eq!(exercise.state().unwrap(), State::Done);
- }
-
- #[test]
- fn test_not_done() {
- assert!(contains_not_done_comment("// I AM NOT DONE"));
- assert!(contains_not_done_comment("/// I AM NOT DONE"));
- assert!(contains_not_done_comment("// I AM NOT DONE"));
- assert!(contains_not_done_comment("/// I AM NOT DONE"));
- assert!(contains_not_done_comment("// I AM NOT DONE "));
- assert!(contains_not_done_comment("// I AM NOT DONE!"));
- assert!(contains_not_done_comment("// I am not done"));
- assert!(contains_not_done_comment("// i am NOT done"));
-
- assert!(!contains_not_done_comment("I AM NOT DONE"));
- assert!(!contains_not_done_comment("// NOT DONE"));
- assert!(!contains_not_done_comment("DONE"));
- }
-}
diff --git a/src/info_file.rs b/src/info_file.rs
new file mode 100644
index 0000000..2a45e02
--- /dev/null
+++ b/src/info_file.rs
@@ -0,0 +1,79 @@
+use anyhow::{bail, Context, Error, Result};
+use serde::Deserialize;
+use std::fs;
+
+// The mode of the exercise.
+#[derive(Deserialize, Copy, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum Mode {
+ // The exercise should be compiled as a binary
+ Run,
+ // The exercise should be compiled as a test harness
+ Test,
+ // The exercise should be linted with clippy
+ Clippy,
+}
+
+// Deserialized from the `info.toml` file.
+#[derive(Deserialize)]
+pub struct ExerciseInfo {
+ // Name of the exercise
+ pub name: String,
+ // The exercise's directory inside the `exercises` directory
+ pub dir: Option<String>,
+ // The mode of the exercise
+ pub mode: Mode,
+ // The hint text associated with the exercise
+ pub hint: String,
+}
+
+impl ExerciseInfo {
+ pub fn path(&self) -> String {
+ if let Some(dir) = &self.dir {
+ format!("exercises/{dir}/{}.rs", self.name)
+ } else {
+ format!("exercises/{}.rs", self.name)
+ }
+ }
+}
+
+#[derive(Deserialize)]
+pub struct InfoFile {
+ pub welcome_message: Option<String>,
+ pub final_message: Option<String>,
+ pub exercises: Vec<ExerciseInfo>,
+}
+
+impl InfoFile {
+ pub fn parse() -> Result<Self> {
+ // Read a local `info.toml` if it exists.
+ let slf: Self = 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")?,
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::NotFound => {
+ toml_edit::de::from_str(include_str!("../info.toml"))
+ .context("Failed to parse the embedded `info.toml` file")?
+ }
+ _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")),
+ },
+ };
+
+ if slf.exercises.is_empty() {
+ bail!("{NO_EXERCISES_ERR}");
+ }
+
+ let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len());
+ for exercise in &slf.exercises {
+ if !names_set.insert(exercise.name.as_str()) {
+ bail!("Exercise names must all be unique!")
+ }
+ }
+ drop(names_set);
+
+ Ok(slf)
+ }
+}
+
+const NO_EXERCISES_ERR: &str = "There are no exercises yet!
+If you are developing third-party exercises, add at least one exercise before testing.";
diff --git a/src/init.rs b/src/init.rs
index 6af3235..459519d 100644
--- a/src/init.rs
+++ b/src/init.rs
@@ -6,17 +6,21 @@ use std::{
path::Path,
};
-use crate::{embedded::EMBEDDED_FILES, exercise::Exercise};
+use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo};
-fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> {
+fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
let mut cargo_toml = Vec::with_capacity(1 << 13);
cargo_toml.extend_from_slice(b"bin = [\n");
- for exercise in exercises {
+ for exercise_info in exercise_infos {
cargo_toml.extend_from_slice(b" { name = \"");
- cargo_toml.extend_from_slice(exercise.name.as_bytes());
- cargo_toml.extend_from_slice(b"\", path = \"");
- cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes());
- cargo_toml.extend_from_slice(b"\" },\n");
+ cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
+ cargo_toml.extend_from_slice(b"\", path = \"exercises/");
+ if let Some(dir) = &exercise_info.dir {
+ cargo_toml.extend_from_slice(dir.as_bytes());
+ cargo_toml.push(b'/');
+ }
+ cargo_toml.extend_from_slice(exercise_info.name.as_bytes());
+ cargo_toml.extend_from_slice(b".rs\" },\n");
}
cargo_toml.extend_from_slice(
@@ -36,46 +40,33 @@ publish = false
}
fn create_gitignore() -> io::Result<()> {
- let gitignore = b"/target";
OpenOptions::new()
.create_new(true)
.write(true)
.open(".gitignore")?
- .write_all(gitignore)
+ .write_all(GITIGNORE)
}
fn create_vscode_dir() -> Result<()> {
create_dir(".vscode").context("Failed to create the directory `.vscode`")?;
- let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
OpenOptions::new()
.create_new(true)
.write(true)
.open(".vscode/extensions.json")?
- .write_all(vs_code_extensions_json)?;
+ .write_all(VS_CODE_EXTENSIONS_JSON)?;
Ok(())
}
-pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> {
+pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
- bail!(
- "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
-in the current directory. It looks like Rustlings was already initialized here.
-Run `rustlings` for instructions on getting started with the exercises.
-
-If you didn't already initialize Rustlings, please initialize it in another directory."
- );
+ bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
}
let rustlings_path = Path::new("rustlings");
if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
- bail!(
- "A directory with the name `rustlings` already exists in the current directory.
-You probably already initialized Rustlings.
-Run `cd rustlings`
-Then run `rustlings` again"
- );
+ bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
return Err(e.into());
}
@@ -87,7 +78,8 @@ Then run `rustlings` again"
.init_exercises_dir()
.context("Failed to initialize the `rustlings/exercises` directory")?;
- create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?;
+ create_cargo_toml(exercise_infos)
+ .context("Failed to create the file `rustlings/Cargo.toml`")?;
create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?;
@@ -95,3 +87,22 @@ Then run `rustlings` again"
Ok(())
}
+
+const GITIGNORE: &[u8] = b"/target
+/.rustlings-state.txt
+";
+
+const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
+
+const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str =
+ "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
+in the current directory. It looks like Rustlings was already initialized here.
+Run `rustlings` for instructions on getting started with the exercises.
+
+If you didn't already initialize Rustlings, please initialize it in another directory.";
+
+const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
+ "A directory with the name `rustlings` already exists in the current directory.
+You probably already initialized Rustlings.
+Run `cd rustlings`
+Then run `rustlings` again";
diff --git a/src/list.rs b/src/list.rs
new file mode 100644
index 0000000..2bb813d
--- /dev/null
+++ b/src/list.rs
@@ -0,0 +1,90 @@
+use anyhow::Result;
+use crossterm::{
+ event::{self, Event, KeyCode, KeyEventKind},
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+ ExecutableCommand,
+};
+use ratatui::{backend::CrosstermBackend, Terminal};
+use std::io;
+
+mod state;
+
+use crate::app_state::AppState;
+
+use self::state::{Filter, UiState};
+
+pub fn list(app_state: &mut AppState) -> Result<()> {
+ let mut stdout = io::stdout().lock();
+ stdout.execute(EnterAlternateScreen)?;
+ enable_raw_mode()?;
+
+ let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
+ terminal.clear()?;
+
+ let mut ui_state = UiState::new(app_state);
+
+ 'outer: loop {
+ terminal.draw(|frame| ui_state.draw(frame).unwrap())?;
+
+ let key = loop {
+ match event::read()? {
+ Event::Key(key) => match key.kind {
+ KeyEventKind::Press | KeyEventKind::Repeat => break key,
+ KeyEventKind::Release => (),
+ },
+ // Redraw
+ Event::Resize(_, _) => continue 'outer,
+ // Ignore
+ Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (),
+ }
+ };
+
+ ui_state.message.clear();
+
+ match key.code {
+ KeyCode::Char('q') => break,
+ KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(),
+ KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(),
+ KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(),
+ KeyCode::End | KeyCode::Char('G') => ui_state.select_last(),
+ KeyCode::Char('d') => {
+ let message = if ui_state.filter == Filter::Done {
+ ui_state.filter = Filter::None;
+ "Disabled filter DONE"
+ } else {
+ ui_state.filter = Filter::Done;
+ "Enabled filter DONE │ Press d again to disable the filter"
+ };
+
+ ui_state = ui_state.with_updated_rows();
+ ui_state.message.push_str(message);
+ }
+ KeyCode::Char('p') => {
+ let message = if ui_state.filter == Filter::Pending {
+ ui_state.filter = Filter::None;
+ "Disabled filter PENDING"
+ } else {
+ ui_state.filter = Filter::Pending;
+ "Enabled filter PENDING │ Press p again to disable the filter"
+ };
+
+ ui_state = ui_state.with_updated_rows();
+ ui_state.message.push_str(message);
+ }
+ KeyCode::Char('r') => {
+ ui_state = ui_state.with_reset_selected()?;
+ }
+ KeyCode::Char('c') => {
+ ui_state.selected_to_current_exercise()?;
+ ui_state = ui_state.with_updated_rows();
+ }
+ _ => (),
+ }
+ }
+
+ drop(terminal);
+ stdout.execute(LeaveAlternateScreen)?;
+ disable_raw_mode()?;
+
+ Ok(())
+}
diff --git a/src/list/state.rs b/src/list/state.rs
new file mode 100644
index 0000000..2a1fef1
--- /dev/null
+++ b/src/list/state.rs
@@ -0,0 +1,261 @@
+use anyhow::{Context, Result};
+use ratatui::{
+ layout::{Constraint, Rect},
+ style::{Style, Stylize},
+ text::Span,
+ widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState},
+ Frame,
+};
+use std::fmt::Write;
+
+use crate::{app_state::AppState, progress_bar::progress_bar_ratatui};
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Filter {
+ Done,
+ Pending,
+ None,
+}
+
+pub struct UiState<'a> {
+ pub table: Table<'static>,
+ pub message: String,
+ pub filter: Filter,
+ app_state: &'a mut AppState,
+ table_state: TableState,
+ n_rows: usize,
+}
+
+impl<'a> UiState<'a> {
+ pub fn with_updated_rows(mut self) -> Self {
+ let current_exercise_ind = self.app_state.current_exercise_ind();
+
+ self.n_rows = 0;
+ let rows = self
+ .app_state
+ .exercises()
+ .iter()
+ .enumerate()
+ .filter_map(|(ind, exercise)| {
+ let exercise_state = if exercise.done {
+ if self.filter == Filter::Pending {
+ return None;
+ }
+
+ "DONE".green()
+ } else {
+ if self.filter == Filter::Done {
+ return None;
+ }
+
+ "PENDING".yellow()
+ };
+
+ self.n_rows += 1;
+
+ let next = if ind == current_exercise_ind {
+ ">>>>".bold().red()
+ } else {
+ Span::default()
+ };
+
+ Some(Row::new([
+ next,
+ exercise_state,
+ Span::raw(exercise.name),
+ Span::raw(exercise.path),
+ ]))
+ });
+
+ self.table = self.table.rows(rows);
+
+ if self.n_rows == 0 {
+ self.table_state.select(None);
+ } else {
+ self.table_state.select(Some(
+ self.table_state
+ .selected()
+ .map_or(0, |selected| selected.min(self.n_rows - 1)),
+ ));
+ }
+
+ self
+ }
+
+ pub fn new(app_state: &'a mut AppState) -> Self {
+ let header = Row::new(["Next", "State", "Name", "Path"]);
+
+ let max_name_len = app_state
+ .exercises()
+ .iter()
+ .map(|exercise| exercise.name.len())
+ .max()
+ .unwrap_or(4) as u16;
+
+ let widths = [
+ Constraint::Length(4),
+ Constraint::Length(7),
+ Constraint::Length(max_name_len),
+ Constraint::Fill(1),
+ ];
+
+ let table = Table::default()
+ .widths(widths)
+ .header(header)
+ .column_spacing(2)
+ .highlight_spacing(HighlightSpacing::Always)
+ .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50)))
+ .highlight_symbol("🦀")
+ .block(Block::default().borders(Borders::BOTTOM));
+
+ let selected = app_state.current_exercise_ind();
+ let table_state = TableState::default()
+ .with_offset(selected.saturating_sub(10))
+ .with_selected(Some(selected));
+
+ let filter = Filter::None;
+ let n_rows = app_state.exercises().len();
+
+ let slf = Self {
+ table,
+ message: String::with_capacity(128),
+ filter,
+ app_state,
+ table_state,
+ n_rows,
+ };
+
+ slf.with_updated_rows()
+ }
+
+ pub fn select_next(&mut self) {
+ if self.n_rows > 0 {
+ let next = self
+ .table_state
+ .selected()
+ .map_or(0, |selected| (selected + 1).min(self.n_rows - 1));
+ self.table_state.select(Some(next));
+ }
+ }
+
+ pub fn select_previous(&mut self) {
+ if self.n_rows > 0 {
+ let previous = self
+ .table_state
+ .selected()
+ .map_or(0, |selected| selected.saturating_sub(1));
+ self.table_state.select(Some(previous));
+ }
+ }
+
+ #[inline]
+ pub fn select_first(&mut self) {
+ if self.n_rows > 0 {
+ self.table_state.select(Some(0));
+ }
+ }
+
+ #[inline]
+ pub fn select_last(&mut self) {
+ if self.n_rows > 0 {
+ self.table_state.select(Some(self.n_rows - 1));
+ }
+ }
+
+ pub fn draw(&mut self, frame: &mut Frame) -> Result<()> {
+ let area = frame.size();
+
+ frame.render_stateful_widget(
+ &self.table,
+ Rect {
+ x: 0,
+ y: 0,
+ width: area.width,
+ height: area.height - 3,
+ },
+ &mut self.table_state,
+ );
+
+ frame.render_widget(
+ Paragraph::new(progress_bar_ratatui(
+ self.app_state.n_done(),
+ self.app_state.exercises().len() as u16,
+ area.width,
+ )?)
+ .block(Block::default().borders(Borders::BOTTOM)),
+ Rect {
+ x: 0,
+ y: area.height - 3,
+ width: area.width,
+ height: 2,
+ },
+ );
+
+ let message = if self.message.is_empty() {
+ // Help footer.
+ Span::raw(
+ "↓/j ↑/k home/g end/G │ filter <d>one/<p>ending │ <r>eset │ <c>ontinue at │ <q>uit",
+ )
+ } else {
+ self.message.as_str().light_blue()
+ };
+ frame.render_widget(
+ message,
+ Rect {
+ x: 0,
+ y: area.height - 1,
+ width: area.width,
+ height: 1,
+ },
+ );
+
+ Ok(())
+ }
+
+ pub fn with_reset_selected(mut self) -> Result<Self> {
+ let Some(selected) = self.table_state.selected() else {
+ return Ok(self);
+ };
+
+ let (ind, exercise) = self
+ .app_state
+ .exercises()
+ .iter()
+ .enumerate()
+ .filter_map(|(ind, exercise)| match self.filter {
+ Filter::Done => exercise.done.then_some((ind, exercise)),
+ Filter::Pending => (!exercise.done).then_some((ind, exercise)),
+ Filter::None => Some((ind, exercise)),
+ })
+ .nth(selected)
+ .context("Invalid selection index")?;
+
+ exercise.reset()?;
+ self.message
+ .write_fmt(format_args!("The exercise {exercise} has been reset!"))?;
+ self.app_state.set_pending(ind)?;
+
+ Ok(self.with_updated_rows())
+ }
+
+ pub fn selected_to_current_exercise(&mut self) -> Result<()> {
+ let Some(selected) = self.table_state.selected() else {
+ return Ok(());
+ };
+
+ let ind = self
+ .app_state
+ .exercises()
+ .iter()
+ .enumerate()
+ .filter_map(|(ind, exercise)| match self.filter {
+ Filter::Done => exercise.done.then_some(ind),
+ Filter::Pending => (!exercise.done).then_some(ind),
+ Filter::None => Some(ind),
+ })
+ .nth(selected)
+ .context("Invalid selection index")?;
+
+ self.app_state.set_current_exercise_ind(ind)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index c8c6584..ed5becf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,59 +1,55 @@
-use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
-use crate::exercise::{Exercise, ExerciseList};
-use crate::run::run;
-use crate::verify::verify;
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result};
+use app_state::StateFileStatus;
use clap::{Parser, Subcommand};
-use console::Emoji;
-use notify_debouncer_mini::notify::RecursiveMode;
-use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
-use shlex::Shlex;
-use std::io::{BufRead, Write};
-use std::path::Path;
-use std::process::{exit, Command};
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::mpsc::{channel, RecvTimeoutError};
-use std::sync::{Arc, Mutex};
-use std::time::Duration;
-use std::{io, thread};
-use verify::VerifyState;
-
-#[macro_use]
-mod ui;
-
+use crossterm::{
+ terminal::{Clear, ClearType},
+ ExecutableCommand,
+};
+use std::{
+ io::{self, BufRead, Write},
+ path::Path,
+ process::exit,
+};
+
+mod app_state;
mod embedded;
mod exercise;
+mod info_file;
mod init;
+mod list;
+mod progress_bar;
mod run;
-mod verify;
+mod watch;
+
+use self::{
+ app_state::AppState,
+ info_file::InfoFile,
+ init::init,
+ list::list,
+ run::run,
+ watch::{watch, WatchExit},
+};
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
#[command(version)]
struct Args {
- /// Show outputs from the test exercises
- #[arg(long)]
- nocapture: bool,
#[command(subcommand)]
command: Option<Subcommands>,
+ /// Manually run the current exercise using `r` or `run` in the watch mode.
+ /// Only use this if Rustlings fails to detect exercise file changes.
+ #[arg(long)]
+ manual_run: bool,
}
#[derive(Subcommand)]
enum Subcommands {
/// Initialize Rustlings
Init,
- /// Verify all exercises according to the recommended order
- Verify,
- /// Rerun `verify` when files were edited
- Watch {
- /// Show hints on success
- #[arg(long)]
- success_hints: bool,
- },
- /// Run/Test a single exercise
+ /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified.
Run {
/// The name of the exercise
- name: String,
+ name: Option<String>,
},
/// Reset a single exercise
Reset {
@@ -65,375 +61,120 @@ enum Subcommands {
/// The name of the exercise
name: String,
},
- /// List the exercises available in Rustlings
- List {
- /// Show only the paths of the exercises
- #[arg(short, long)]
- paths: bool,
- /// Show only the names of the exercises
- #[arg(short, long)]
- names: bool,
- /// Provide a string to match exercise names.
- /// Comma separated patterns are accepted
- #[arg(short, long)]
- filter: Option<String>,
- /// Display only exercises not yet solved
- #[arg(short, long)]
- unsolved: bool,
- /// Display only exercises that have been solved
- #[arg(short, long)]
- solved: bool,
- },
}
fn main() -> Result<()> {
let args = Args::parse();
- if args.command.is_none() {
- println!("\n{WELCOME}\n");
- }
-
- which::which("cargo").context(
- "Failed to find `cargo`.
-Did you already install Rust?
-Try running `cargo --version` to diagnose the problem.",
- )?;
+ which::which("cargo").context(CARGO_NOT_FOUND_ERR)?;
- let exercises = ExerciseList::parse()?.exercises;
+ let info_file = InfoFile::parse()?;
if matches!(args.command, Some(Subcommands::Init)) {
- init::init_rustlings(&exercises).context("Initialization failed")?;
- println!(
- "\nDone initialization!\n
-Run `cd rustlings` to go into the generated directory.
-Then run `rustlings` for further instructions on getting started."
- );
+ init(&info_file.exercises).context("Initialization failed")?;
+
+ println!("{POST_INIT_MSG}");
return Ok(());
} else if !Path::new("exercises").is_dir() {
- println!(
- "\nThe `exercises` directory wasn't found in the current directory.
-If you are just starting with Rustlings, run the command `rustlings init` to initialize it."
- );
+ println!("{PRE_INIT_MSG}");
exit(1);
}
- let verbose = args.nocapture;
- let command = args.command.unwrap_or_else(|| {
- println!("{DEFAULT_OUT}\n");
- exit(0);
- });
-
- match command {
- // `Init` is handled above.
- Subcommands::Init => (),
- Subcommands::List {
- paths,
- names,
- filter,
- unsolved,
- solved,
- } => {
- if !paths && !names {
- println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
- }
- let mut exercises_done: u16 = 0;
- let lowercase_filter = filter
- .as_ref()
- .map(|s| s.to_lowercase())
- .unwrap_or_default();
- let filters = lowercase_filter
- .split(',')
- .filter_map(|f| {
- let f = f.trim();
- if f.is_empty() {
- None
- } else {
- Some(f)
- }
- })
- .collect::<Vec<_>>();
-
- for exercise in &exercises {
- let fname = exercise.path.to_string_lossy();
- let filter_cond = filters
- .iter()
- .any(|f| exercise.name.contains(f) || fname.contains(f));
- let looks_done = exercise.looks_done()?;
- let status = if looks_done {
- exercises_done += 1;
- "Done"
- } else {
- "Pending"
- };
- let solve_cond =
- (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved);
- if solve_cond && (filter_cond || filter.is_none()) {
- let line = if paths {
- format!("{fname}\n")
- } else if names {
- format!("{}\n", exercise.name)
- } else {
- format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name)
- };
- // Somehow using println! leads to the binary panicking
- // when its output is piped.
- // So, we're handling a Broken Pipe error and exiting with 0 anyway
- let stdout = std::io::stdout();
- {
- let mut handle = stdout.lock();
- handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
- match e.kind() {
- std::io::ErrorKind::BrokenPipe => exit(0),
- _ => exit(1),
- };
- });
- }
- }
- }
-
- let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0;
- println!(
- "Progress: You completed {} / {} exercises ({:.1} %).",
- exercises_done,
- exercises.len(),
- percentage_progress
- );
- exit(0);
- }
+ let (mut app_state, state_file_status) = AppState::new(
+ info_file.exercises,
+ info_file.final_message.unwrap_or_default(),
+ );
- Subcommands::Run { name } => {
- let exercise = find_exercise(&name, &exercises)?;
- run(exercise, verbose).unwrap_or_else(|_| exit(1));
- }
+ if let Some(welcome_message) = info_file.welcome_message {
+ match state_file_status {
+ StateFileStatus::NotRead => {
+ let mut stdout = io::stdout().lock();
+ stdout.execute(Clear(ClearType::All))?;
- Subcommands::Reset { name } => {
- let exercise = find_exercise(&name, &exercises)?;
- EMBEDDED_FILES
- .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite)
- .with_context(|| format!("Failed to reset the exercise {exercise}"))?;
- println!("The file {} has been reset!", exercise.path.display());
- }
+ let welcome_message = welcome_message.trim();
+ write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
+ stdout.flush()?;
- Subcommands::Hint { name } => {
- let exercise = find_exercise(&name, &exercises)?;
- println!("{}", exercise.hint);
- }
-
- Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? {
- VerifyState::AllExercisesDone => println!("All exercises done!"),
- VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"),
- },
+ io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
- Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
- Err(e) => {
- println!("Error: Could not watch your progress. Error message was {e:?}.");
- println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
- exit(1);
- }
- Ok(WatchStatus::Finished) => {
- println!(
- "{emoji} All exercises completed! {emoji}",
- emoji = Emoji("🎉", "★")
- );
- println!("\n{FENISH_LINE}\n");
- }
- Ok(WatchStatus::Unfinished) => {
- println!("We hope you're enjoying learning about Rust!");
- println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");
+ stdout.execute(Clear(ClearType::All))?;
}
- },
+ StateFileStatus::Read => (),
+ }
}
- Ok(())
-}
-
-fn spawn_watch_shell(
- failed_exercise_hint: Arc<Mutex<Option<String>>>,
- should_quit: Arc<AtomicBool>,
-) {
- println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
-
- thread::spawn(move || {
- let mut input = String::with_capacity(32);
- let mut stdin = io::stdin().lock();
-
- loop {
- // Recycle input buffer.
- input.clear();
-
- if let Err(e) = stdin.read_line(&mut input) {
- println!("error reading command: {e}");
- }
-
- let input = input.trim();
- if input == "hint" {
- if let Some(hint) = &*failed_exercise_hint.lock().unwrap() {
- println!("{hint}");
- }
- } else if input == "clear" {
- println!("\x1B[2J\x1B[1;1H");
- } else if input == "quit" {
- should_quit.store(true, Ordering::SeqCst);
- println!("Bye!");
- } else if input == "help" {
- println!("{WATCH_MODE_HELP_MESSAGE}");
- } else if let Some(cmd) = input.strip_prefix('!') {
- let mut parts = Shlex::new(cmd);
-
- let Some(program) = parts.next() else {
- println!("no command provided");
- continue;
- };
-
- if let Err(e) = Command::new(program).args(parts).status() {
- println!("failed to execute command `{cmd}`: {e}");
- }
+ match args.command {
+ None => {
+ let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run {
+ None
} else {
- println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
+ // For the the notify event handler thread.
+ // Leaking is not a problem because the slice lives until the end of the program.
+ Some(
+ app_state
+ .exercises()
+ .iter()
+ .map(|exercise| exercise.path)
+ .collect::<Vec<_>>()
+ .leak(),
+ )
+ };
+
+ loop {
+ match watch(&mut app_state, notify_exercise_paths)? {
+ WatchExit::Shutdown => break,
+ // It is much easier to exit the watch mode, launch the list mode and then restart
+ // the watch mode instead of trying to pause the watch threads and correct the
+ // watch state.
+ WatchExit::List => list(&mut app_state)?,
+ }
}
}
- });
-}
-
-fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> {
- if name == "next" {
- for exercise in exercises {
- if !exercise.looks_done()? {
- return Ok(exercise);
+ // `Init` is handled above.
+ Some(Subcommands::Init) => (),
+ Some(Subcommands::Run { name }) => {
+ if let Some(name) = name {
+ app_state.set_current_exercise_by_name(&name)?;
}
+ run(&mut app_state)?;
}
-
- println!("🎉 Congratulations! You have done all the exercises!");
- println!("🔚 There are no more exercises to do next!");
- exit(0);
- }
-
- exercises
- .iter()
- .find(|e| e.name == name)
- .with_context(|| format!("No exercise found for '{name}'!"))
-}
-
-enum WatchStatus {
- Finished,
- Unfinished,
-}
-
-fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result<WatchStatus> {
- /* Clears the terminal with an ANSI escape code.
- Works in UNIX and newer Windows terminals. */
- fn clear_screen() {
- println!("\x1Bc");
- }
-
- let (tx, rx) = channel();
- let should_quit = Arc::new(AtomicBool::new(false));
-
- let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
- debouncer
- .watcher()
- .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
-
- clear_screen();
-
- let failed_exercise_hint =
- match verify(exercises, (0, exercises.len()), verbose, success_hints)? {
- VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
- VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
- };
-
- spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
-
- let mut pending_exercises = Vec::with_capacity(exercises.len());
- loop {
- match rx.recv_timeout(Duration::from_secs(1)) {
- Ok(event) => match event {
- Ok(events) => {
- for event in events {
- if event.kind == DebouncedEventKind::Any
- && event.path.extension().is_some_and(|ext| ext == "rs")
- {
- pending_exercises.extend(exercises.iter().filter(|exercise| {
- !exercise.looks_done().unwrap_or(false)
- || event.path.ends_with(&exercise.path)
- }));
- let num_done = exercises.len() - pending_exercises.len();
-
- clear_screen();
-
- match verify(
- pending_exercises.iter().copied(),
- (num_done, exercises.len()),
- verbose,
- success_hints,
- )? {
- VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished),
- VerifyState::Failed(exercise) => {
- let hint = exercise.hint.clone();
- *failed_exercise_hint.lock().unwrap() = Some(hint);
- }
- }
-
- pending_exercises.clear();
- }
- }
- }
- Err(e) => println!("watch error: {e:?}"),
- },
- Err(RecvTimeoutError::Timeout) => {
- // the timeout expired, just check the `should_quit` variable below then loop again
- }
- Err(e) => println!("watch error: {e:?}"),
+ Some(Subcommands::Reset { name }) => {
+ app_state.set_current_exercise_by_name(&name)?;
+ let exercise = app_state.current_exercise();
+ exercise.reset()?;
+ println!("The exercise {exercise} has been reset!");
+ app_state.set_pending(app_state.current_exercise_ind())?;
}
- // Check if we need to exit
- if should_quit.load(Ordering::SeqCst) {
- return Ok(WatchStatus::Unfinished);
+ Some(Subcommands::Hint { name }) => {
+ app_state.set_current_exercise_by_name(&name)?;
+ println!("{}", app_state.current_exercise().hint);
}
}
+
+ Ok(())
}
-const WELCOME: &str = r" welcome to...
+const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`.
+Did you already install Rust?
+Try running `cargo --version` to diagnose the problem.";
+
+const PRE_INIT_MSG: &str = r"
+ welcome to...
_ _ _
_ __ _ _ ___| |_| (_)_ __ __ _ ___
| '__| | | / __| __| | | '_ \ / _` / __|
| | | |_| \__ \ |_| | | | | | (_| \__ \
|_| \__,_|___/\__|_|_|_| |_|\__, |___/
- |___/";
+ |___/
-const DEFAULT_OUT: &str =
- "Is this your first time? Don't worry, Rustlings was made for beginners! We are
-going to teach you a lot of things about Rust, but before we can get
-started, here's a couple of notes about how Rustlings operates:
+The `exercises` directory wasn't found in the current directory.
+If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";
-1. The central concept behind Rustlings is that you solve exercises. These
- exercises usually have some sort of syntax error in them, which will cause
- them to fail compilation or testing. Sometimes there's a logic error instead
- of a syntax error. No matter what error, it's your job to find it and fix it!
- You'll know when you fixed it because then, the exercise will compile and
- Rustlings will be able to move on to the next exercise.
-2. If you run Rustlings in watch mode (which we recommend), it'll automatically
- start with the first exercise. Don't get confused by an error message popping
- up as soon as you run Rustlings! This is part of the exercise that you're
- supposed to solve, so open the exercise file in an editor and start your
- detective work!
-3. If you're stuck on an exercise, there is a helpful hint you can view by typing
- 'hint' (in watch mode), or running `rustlings hint exercise_name`.
-4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
- (https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
- and sometimes, other learners do too so you can help each other out!
+const POST_INIT_MSG: &str = "
+Done initialization!
-Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise.
-Make sure to have your editor open in the `rustlings` directory!";
-
-const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode:
- hint - prints the current exercise's hint
- clear - clears the screen
- quit - quits watch mode
- !<cmd> - executes a command, like `!rustc --explain E0381`
- help - displays this help message
-
-Watch mode automatically re-evaluates the current exercise
-when you edit a file's contents.";
+Run `cd rustlings` to go into the generated directory.
+Then run `rustlings` to get started.";
const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! |
@@ -446,7 +187,7 @@ const FENISH_LINE: &str = "+----------------------------------------------------
▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓
▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒
▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒
- ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒
+ ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒
▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒
@@ -455,9 +196,4 @@ const FENISH_LINE: &str = "+----------------------------------------------------
▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒
▒▒ ▒▒ ▒▒ ▒▒\x1b[0m
-We hope you enjoyed learning about the various aspects of Rust!
-If you noticed any issues, please don't hesitate to report them to our repo.
-You can also contribute your own exercises to help the greater community!
-
-Before reporting an issue or contributing, please read our guidelines:
-https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md";
+";
diff --git a/src/progress_bar.rs b/src/progress_bar.rs
new file mode 100644
index 0000000..d6962b8
--- /dev/null
+++ b/src/progress_bar.rs
@@ -0,0 +1,97 @@
+use anyhow::{bail, Result};
+use ratatui::text::{Line, Span};
+use std::fmt::Write;
+
+const PREFIX: &str = "Progress: [";
+const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
+// Leaving the last char empty (_) for `total` > 99.
+const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16;
+const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
+const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
+
+const PROGRESS_EXCEEDS_MAX_ERR: &str =
+ "The progress of the progress bar is higher than the maximum";
+
+pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
+ use crossterm::style::Stylize;
+
+ if progress > total {
+ bail!(PROGRESS_EXCEEDS_MAX_ERR);
+ }
+
+ if line_width < MIN_LINE_WIDTH {
+ return Ok(format!("Progress: {progress}/{total} exercises"));
+ }
+
+ let mut line = String::with_capacity(usize::from(line_width));
+ line.push_str(PREFIX);
+
+ let width = line_width - WRAPPER_WIDTH;
+ let filled = (width * progress) / total;
+
+ let mut green_part = String::with_capacity(usize::from(filled + 1));
+ for _ in 0..filled {
+ green_part.push('#');
+ }
+
+ if filled < width {
+ green_part.push('>');
+ }
+ write!(line, "{}", green_part.green()).unwrap();
+
+ let width_minus_filled = width - filled;
+ if width_minus_filled > 1 {
+ let red_part_width = width_minus_filled - 1;
+ let mut red_part = String::with_capacity(usize::from(red_part_width));
+ for _ in 0..red_part_width {
+ red_part.push('-');
+ }
+ write!(line, "{}", red_part.red()).unwrap();
+ }
+
+ writeln!(line, "] {progress:>3}/{total} exercises").unwrap();
+
+ Ok(line)
+}
+
+pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
+ use ratatui::style::Stylize;
+
+ if progress > total {
+ bail!(PROGRESS_EXCEEDS_MAX_ERR);
+ }
+
+ if line_width < MIN_LINE_WIDTH {
+ return Ok(Line::raw(format!("Progress: {progress}/{total} exercises")));
+ }
+
+ let mut spans = Vec::with_capacity(4);
+ spans.push(Span::raw(PREFIX));
+
+ let width = line_width - WRAPPER_WIDTH;
+ let filled = (width * progress) / total;
+
+ let mut green_part = String::with_capacity(usize::from(filled + 1));
+ for _ in 0..filled {
+ green_part.push('#');
+ }
+
+ if filled < width {
+ green_part.push('>');
+ }
+ spans.push(green_part.green());
+
+ let width_minus_filled = width - filled;
+ if width_minus_filled > 1 {
+ let red_part_width = width_minus_filled - 1;
+ let mut red_part = String::with_capacity(usize::from(red_part_width));
+ for _ in 0..red_part_width {
+ red_part.push('-');
+ }
+ spans.push(red_part.red());
+ }
+
+ spans.push(Span::raw(format!("] {progress:>3}/{total} exercises")));
+
+ Ok(Line::from(spans))
+}
diff --git a/src/run.rs b/src/run.rs
index 3f93f14..863b584 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,39 +1,41 @@
use anyhow::{bail, Result};
-use std::io::{stdout, Write};
-use std::time::Duration;
-
-use crate::exercise::{Exercise, Mode};
-use crate::verify::test;
-use indicatif::ProgressBar;
-
-// Invoke the rust compiler on the path of the given exercise,
-// and run the ensuing binary.
-// The verbose argument helps determine whether or not to show
-// the output from the test harnesses (if the mode of the exercise is test)
-pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> {
- match exercise.mode {
- Mode::Test => test(exercise, verbose),
- Mode::Compile | Mode::Clippy => compile_and_run(exercise),
- }
-}
+use crossterm::style::Stylize;
+use std::io::{self, Write};
-// Compile and run an exercise.
-// This is strictly for non-test binaries, so output is displayed
-fn compile_and_run(exercise: &Exercise) -> Result<()> {
- let progress_bar = ProgressBar::new_spinner();
- progress_bar.set_message(format!("Running {exercise}..."));
- progress_bar.enable_steady_tick(Duration::from_millis(100));
+use crate::app_state::{AppState, ExercisesProgress};
+pub fn run(app_state: &mut AppState) -> Result<()> {
+ let exercise = app_state.current_exercise();
let output = exercise.run()?;
- progress_bar.finish_and_clear();
- stdout().write_all(&output.stdout)?;
+ let mut stdout = io::stdout().lock();
+ stdout.write_all(&output.stdout)?;
+ stdout.write_all(b"\n")?;
+ stdout.write_all(&output.stderr)?;
+ stdout.flush()?;
+
if !output.status.success() {
- stdout().write_all(&output.stderr)?;
- warn!("Ran {} with errors", exercise);
- bail!("TODO");
+ app_state.set_pending(app_state.current_exercise_ind())?;
+
+ bail!(
+ "Ran {} with errors",
+ app_state.current_exercise().terminal_link(),
+ );
+ }
+
+ stdout.write_fmt(format_args!(
+ "{}{}\n",
+ "✓ Successfully ran ".green(),
+ exercise.path.green(),
+ ))?;
+
+ match app_state.done_current_exercise(&mut stdout)? {
+ ExercisesProgress::AllDone => (),
+ ExercisesProgress::Pending => println!(
+ "Next exercise: {}",
+ app_state.current_exercise().terminal_link(),
+ ),
}
- success!("Successfully ran {}", exercise);
Ok(())
}
diff --git a/src/ui.rs b/src/ui.rs
deleted file mode 100644
index 22d60d9..0000000
--- a/src/ui.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-macro_rules! print_emoji {
- ($emoji:expr, $sign:expr, $color: ident, $fmt:literal, $ex:expr) => {{
- use console::{style, Emoji};
- use std::env;
- let formatstr = format!($fmt, $ex);
- if env::var_os("NO_EMOJI").is_some() {
- println!("{} {}", style($sign).$color(), style(formatstr).$color());
- } else {
- println!(
- "{} {}",
- style(Emoji($emoji, $sign)).$color(),
- style(formatstr).$color()
- );
- }
- }};
-}
-
-macro_rules! warn {
- ($fmt:literal, $ex:expr) => {{
- print_emoji!("⚠️ ", "!", red, $fmt, $ex);
- }};
-}
-
-macro_rules! success {
- ($fmt:literal, $ex:expr) => {{
- print_emoji!("✅ ", "✓", green, $fmt, $ex);
- }};
-}
diff --git a/src/verify.rs b/src/verify.rs
deleted file mode 100644
index ef966f6..0000000
--- a/src/verify.rs
+++ /dev/null
@@ -1,223 +0,0 @@
-use anyhow::{bail, Result};
-use console::style;
-use indicatif::{ProgressBar, ProgressStyle};
-use std::{
- env,
- io::{stdout, Write},
- process::Output,
- time::Duration,
-};
-
-use crate::exercise::{Exercise, Mode, State};
-
-pub enum VerifyState<'a> {
- AllExercisesDone,
- Failed(&'a Exercise),
-}
-
-// Verify that the provided container of Exercise objects
-// can be compiled and run without any failures.
-// Any such failures will be reported to the end user.
-// If the Exercise being verified is a test, the verbose boolean
-// determines whether or not the test harness outputs are displayed.
-pub fn verify<'a>(
- pending_exercises: impl IntoIterator<Item = &'a Exercise>,
- progress: (usize, usize),
- verbose: bool,
- success_hints: bool,
-) -> Result<VerifyState<'a>> {
- let (num_done, total) = progress;
- let bar = ProgressBar::new(total as u64);
- let mut percentage = num_done as f32 / total as f32 * 100.0;
- bar.set_style(
- ProgressStyle::default_bar()
- .template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}")
- .expect("Progressbar template should be valid!")
- .progress_chars("#>-"),
- );
- bar.set_position(num_done as u64);
- bar.set_message(format!("({percentage:.1} %)"));
-
- for exercise in pending_exercises {
- let compile_result = match exercise.mode {
- Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?,
- Mode::Compile => compile_and_run_interactively(exercise, success_hints)?,
- Mode::Clippy => compile_only(exercise, success_hints)?,
- };
- if !compile_result {
- return Ok(VerifyState::Failed(exercise));
- }
- percentage += 100.0 / total as f32;
- bar.inc(1);
- bar.set_message(format!("({percentage:.1} %)"));
- }
-
- bar.finish();
- println!("You completed all exercises!");
-
- Ok(VerifyState::AllExercisesDone)
-}
-
-#[derive(PartialEq, Eq)]
-enum RunMode {
- Interactive,
- NonInteractive,
-}
-
-// Compile and run the resulting test harness of the given Exercise
-pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> {
- compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?;
- Ok(())
-}
-
-// Invoke the rust compiler without running the resulting binary
-fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool> {
- let progress_bar = ProgressBar::new_spinner();
- progress_bar.set_message(format!("Compiling {exercise}..."));
- progress_bar.enable_steady_tick(Duration::from_millis(100));
-
- let _ = exercise.run()?;
- progress_bar.finish_and_clear();
-
- prompt_for_completion(exercise, None, success_hints)
-}
-
-// Compile the given Exercise and run the resulting binary in an interactive mode
-fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool> {
- let progress_bar = ProgressBar::new_spinner();
- progress_bar.set_message(format!("Running {exercise}..."));
- progress_bar.enable_steady_tick(Duration::from_millis(100));
-
- let output = exercise.run()?;
- progress_bar.finish_and_clear();
-
- if !output.status.success() {
- warn!("Ran {} with errors", exercise);
- {
- let mut stdout = stdout().lock();
- stdout.write_all(&output.stdout)?;
- stdout.write_all(&output.stderr)?;
- stdout.flush()?;
- }
- bail!("TODO");
- }
-
- prompt_for_completion(exercise, Some(output), success_hints)
-}
-
-// Compile the given Exercise as a test harness and display
-// the output if verbose is set to true
-fn compile_and_test(
- exercise: &Exercise,
- run_mode: RunMode,
- verbose: bool,
- success_hints: bool,
-) -> Result<bool> {
- let progress_bar = ProgressBar::new_spinner();
- progress_bar.set_message(format!("Testing {exercise}..."));
- progress_bar.enable_steady_tick(Duration::from_millis(100));
-
- let output = exercise.run()?;
- progress_bar.finish_and_clear();
-
- if !output.status.success() {
- warn!(
- "Testing of {} failed! Please try again. Here's the output:",
- exercise
- );
- {
- let mut stdout = stdout().lock();
- stdout.write_all(&output.stdout)?;
- stdout.write_all(&output.stderr)?;
- stdout.flush()?;
- }
- bail!("TODO");
- }
-
- if verbose {
- stdout().write_all(&output.stdout)?;
- }
-
- if run_mode == RunMode::Interactive {
- prompt_for_completion(exercise, None, success_hints)
- } else {
- Ok(true)
- }
-}
-
-fn prompt_for_completion(
- exercise: &Exercise,
- prompt_output: Option<Output>,
- success_hints: bool,
-) -> Result<bool> {
- let context = match exercise.state()? {
- State::Done => return Ok(true),
- State::Pending(context) => context,
- };
- match exercise.mode {
- Mode::Compile => success!("Successfully ran {}!", exercise),
- Mode::Test => success!("Successfully tested {}!", exercise),
- Mode::Clippy => success!("Successfully compiled {}!", exercise),
- }
-
- let no_emoji = env::var("NO_EMOJI").is_ok();
-
- let clippy_success_msg = if no_emoji {
- "The code is compiling, and Clippy is happy!"
- } else {
- "The code is compiling, and 📎 Clippy 📎 is happy!"
- };
-
- let success_msg = match exercise.mode {
- Mode::Compile => "The code is compiling!",
- Mode::Test => "The code is compiling, and the tests pass!",
- Mode::Clippy => clippy_success_msg,
- };
-
- if no_emoji {
- println!("\n~*~ {success_msg} ~*~\n");
- } else {
- println!("\n🎉 🎉 {success_msg} 🎉 🎉\n");
- }
-
- if let Some(output) = prompt_output {
- let separator = separator();
- println!("Output:\n{separator}");
- stdout().write_all(&output.stdout).unwrap();
- println!("\n{separator}\n");
- }
- if success_hints {
- println!(
- "Hints:\n{separator}\n{}\n{separator}\n",
- exercise.hint,
- separator = separator(),
- );
- }
-
- println!("You can keep working on this exercise,");
- println!(
- "or jump into the next one by removing the {} comment:",
- style("`I AM NOT DONE`").bold()
- );
- println!();
- for context_line in context {
- let formatted_line = if context_line.important {
- format!("{}", style(context_line.line).bold())
- } else {
- context_line.line
- };
-
- println!(
- "{:>2} {} {}",
- style(context_line.number).blue().bold(),
- style("|").blue(),
- formatted_line,
- );
- }
-
- Ok(false)
-}
-
-fn separator() -> console::StyledObject<&'static str> {
- style("====================").bold()
-}
diff --git a/src/watch.rs b/src/watch.rs
new file mode 100644
index 0000000..d20e552
--- /dev/null
+++ b/src/watch.rs
@@ -0,0 +1,128 @@
+use anyhow::{Error, Result};
+use notify_debouncer_mini::{
+ new_debouncer,
+ notify::{self, RecursiveMode},
+};
+use std::{
+ io::{self, Write},
+ path::Path,
+ sync::mpsc::channel,
+ thread,
+ time::Duration,
+};
+
+mod notify_event;
+mod state;
+mod terminal_event;
+
+use crate::app_state::{AppState, ExercisesProgress};
+
+use self::{
+ notify_event::DebounceEventHandler,
+ state::WatchState,
+ terminal_event::{terminal_event_handler, InputEvent},
+};
+
+enum WatchEvent {
+ Input(InputEvent),
+ FileChange { exercise_ind: usize },
+ TerminalResize,
+ NotifyErr(notify::Error),
+ TerminalEventErr(io::Error),
+}
+
+/// Returned by the watch mode to indicate what to do afterwards.
+#[must_use]
+pub enum WatchExit {
+ /// Exit the program.
+ Shutdown,
+ /// Enter the list mode and restart the watch mode afterwards.
+ List,
+}
+
+pub fn watch(
+ app_state: &mut AppState,
+ notify_exercise_paths: Option<&'static [&'static str]>,
+) -> Result<WatchExit> {
+ let (tx, rx) = channel();
+
+ let mut manual_run = false;
+ // Prevent dropping the guard until the end of the function.
+ // Otherwise, the file watcher exits.
+ let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths {
+ let mut debouncer = new_debouncer(
+ Duration::from_secs(1),
+ DebounceEventHandler {
+ tx: tx.clone(),
+ exercise_paths,
+ },
+ )
+ .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
+ debouncer
+ .watcher()
+ .watch(Path::new("exercises"), RecursiveMode::Recursive)
+ .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
+
+ Some(debouncer)
+ } else {
+ manual_run = true;
+ None
+ };
+
+ let mut watch_state = WatchState::new(app_state, manual_run);
+
+ watch_state.run_current_exercise()?;
+
+ thread::spawn(move || terminal_event_handler(tx, manual_run));
+
+ while let Ok(event) = rx.recv() {
+ match event {
+ WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
+ ExercisesProgress::AllDone => break,
+ ExercisesProgress::Pending => watch_state.run_current_exercise()?,
+ },
+ WatchEvent::Input(InputEvent::Hint) => {
+ watch_state.show_hint()?;
+ }
+ WatchEvent::Input(InputEvent::List) => {
+ return Ok(WatchExit::List);
+ }
+ WatchEvent::Input(InputEvent::Quit) => {
+ watch_state.into_writer().write_all(QUIT_MSG)?;
+ break;
+ }
+ WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
+ WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
+ watch_state.handle_invalid_cmd(&cmd)?;
+ }
+ WatchEvent::FileChange { exercise_ind } => {
+ watch_state.run_exercise_with_ind(exercise_ind)?;
+ }
+ WatchEvent::TerminalResize => {
+ watch_state.render()?;
+ }
+ WatchEvent::NotifyErr(e) => {
+ watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
+ return Err(Error::from(e));
+ }
+ WatchEvent::TerminalEventErr(e) => {
+ return Err(Error::from(e).context("Terminal event listener failed"));
+ }
+ }
+ }
+
+ Ok(WatchExit::Shutdown)
+}
+
+const QUIT_MSG: &[u8] = b"
+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.
+";
+
+const NOTIFY_ERR: &str = "
+The automatic detection of exercise file changes failed :(
+Please try running `rustlings` again.
+
+If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
+You need to manually trigger running the current exercise using `r` or `run` then.
+";
diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs
new file mode 100644
index 0000000..fb9a8c0
--- /dev/null
+++ b/src/watch/notify_event.rs
@@ -0,0 +1,42 @@
+use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
+use std::sync::mpsc::Sender;
+
+use super::WatchEvent;
+
+pub struct DebounceEventHandler {
+ pub tx: Sender<WatchEvent>,
+ pub exercise_paths: &'static [&'static str],
+}
+
+impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
+ fn handle_event(&mut self, event: DebounceEventResult) {
+ let event = match event {
+ Ok(event) => {
+ let Some(exercise_ind) = event
+ .iter()
+ .filter_map(|event| {
+ if event.kind != DebouncedEventKind::Any
+ || !event.path.extension().is_some_and(|ext| ext == "rs")
+ {
+ return None;
+ }
+
+ self.exercise_paths
+ .iter()
+ .position(|path| event.path.ends_with(path))
+ })
+ .min()
+ else {
+ return;
+ };
+
+ WatchEvent::FileChange { exercise_ind }
+ }
+ Err(e) => WatchEvent::NotifyErr(e),
+ };
+
+ // An error occurs when the receiver is dropped.
+ // After dropping the receiver, the debouncer guard should also be dropped.
+ let _ = self.tx.send(event);
+ }
+}
diff --git a/src/watch/state.rs b/src/watch/state.rs
new file mode 100644
index 0000000..c0f6c53
--- /dev/null
+++ b/src/watch/state.rs
@@ -0,0 +1,168 @@
+use anyhow::Result;
+use crossterm::{
+ style::Stylize,
+ terminal::{size, Clear, ClearType},
+ ExecutableCommand,
+};
+use std::io::{self, StdoutLock, Write};
+
+use crate::{
+ app_state::{AppState, ExercisesProgress},
+ progress_bar::progress_bar,
+};
+
+pub struct WatchState<'a> {
+ writer: StdoutLock<'a>,
+ app_state: &'a mut AppState,
+ stdout: Option<Vec<u8>>,
+ stderr: Option<Vec<u8>>,
+ show_hint: bool,
+ show_done: bool,
+ manual_run: bool,
+}
+
+impl<'a> WatchState<'a> {
+ pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self {
+ let writer = io::stdout().lock();
+
+ Self {
+ writer,
+ app_state,
+ stdout: None,
+ stderr: None,
+ show_hint: false,
+ show_done: false,
+ manual_run,
+ }
+ }
+
+ #[inline]
+ pub fn into_writer(self) -> StdoutLock<'a> {
+ self.writer
+ }
+
+ pub fn run_current_exercise(&mut self) -> Result<()> {
+ self.show_hint = false;
+
+ let output = self.app_state.current_exercise().run()?;
+ self.stdout = Some(output.stdout);
+
+ if output.status.success() {
+ self.stderr = None;
+ self.show_done = true;
+ } else {
+ self.app_state
+ .set_pending(self.app_state.current_exercise_ind())?;
+
+ self.stderr = Some(output.stderr);
+ self.show_done = false;
+ }
+
+ self.render()
+ }
+
+ pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
+ self.app_state.set_current_exercise_ind(exercise_ind)?;
+ self.run_current_exercise()
+ }
+
+ pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
+ if !self.show_done {
+ self.writer
+ .write_all(b"The current exercise isn't done yet\n")?;
+ self.show_prompt()?;
+ return Ok(ExercisesProgress::Pending);
+ }
+
+ self.app_state.done_current_exercise(&mut self.writer)
+ }
+
+ fn show_prompt(&mut self) -> io::Result<()> {
+ self.writer.write_all(b"\n")?;
+
+ if self.manual_run {
+ self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?;
+ }
+
+ if self.show_done {
+ self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?;
+ }
+
+ if !self.show_hint {
+ self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?;
+ }
+
+ self.writer
+ .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?;
+
+ self.writer.flush()
+ }
+
+ pub fn render(&mut self) -> Result<()> {
+ // Prevent having the first line shifted.
+ self.writer.write_all(b"\n")?;
+
+ self.writer.execute(Clear(ClearType::All))?;
+
+ if let Some(stdout) = &self.stdout {
+ self.writer.write_all(stdout)?;
+ self.writer.write_all(b"\n")?;
+ }
+
+ if let Some(stderr) = &self.stderr {
+ self.writer.write_all(stderr)?;
+ self.writer.write_all(b"\n")?;
+ }
+
+ self.writer.write_all(b"\n")?;
+
+ if self.show_hint {
+ self.writer.write_fmt(format_args!(
+ "{}\n{}\n\n",
+ "Hint".bold().cyan().underlined(),
+ self.app_state.current_exercise().hint,
+ ))?;
+ }
+
+ if self.show_done {
+ self.writer.write_fmt(format_args!(
+ "{}\n\n",
+ "Exercise done ✓
+When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀"
+ .bold()
+ .green(),
+ ))?;
+ }
+
+ let line_width = size()?.0;
+ let progress_bar = progress_bar(
+ self.app_state.n_done(),
+ self.app_state.exercises().len() as u16,
+ line_width,
+ )?;
+ self.writer.write_fmt(format_args!(
+ "{progress_bar}Current exercise: {}\n",
+ self.app_state.current_exercise().terminal_link(),
+ ))?;
+
+ self.show_prompt()?;
+
+ Ok(())
+ }
+
+ pub fn show_hint(&mut self) -> Result<()> {
+ self.show_hint = true;
+ self.render()
+ }
+
+ pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> {
+ self.writer.write_all(b"Invalid command: ")?;
+ self.writer.write_all(cmd.as_bytes())?;
+ if cmd.len() > 1 {
+ self.writer
+ .write_all(b" (confusing input can occur after resizing the terminal)")?;
+ }
+ self.writer.write_all(b"\n")?;
+ self.show_prompt()
+ }
+}
diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs
new file mode 100644
index 0000000..6d790b7
--- /dev/null
+++ b/src/watch/terminal_event.rs
@@ -0,0 +1,73 @@
+use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
+use std::sync::mpsc::Sender;
+
+use super::WatchEvent;
+
+pub enum InputEvent {
+ Run,
+ Next,
+ Hint,
+ List,
+ Quit,
+ Unrecognized(String),
+}
+
+pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
+ let mut input = String::with_capacity(8);
+
+ let last_input_event = loop {
+ let terminal_event = match event::read() {
+ Ok(v) => v,
+ Err(e) => {
+ // If `send` returns an error, then the receiver is dropped and
+ // a shutdown has been already initialized.
+ let _ = tx.send(WatchEvent::TerminalEventErr(e));
+ return;
+ }
+ };
+
+ match terminal_event {
+ Event::Key(key) => {
+ if key.modifiers != KeyModifiers::NONE {
+ continue;
+ }
+
+ match key.kind {
+ KeyEventKind::Release => continue,
+ KeyEventKind::Press | KeyEventKind::Repeat => (),
+ }
+
+ match key.code {
+ KeyCode::Enter => {
+ let input_event = match input.trim() {
+ "n" | "next" => InputEvent::Next,
+ "h" | "hint" => InputEvent::Hint,
+ "l" | "list" => break InputEvent::List,
+ "q" | "quit" => break InputEvent::Quit,
+ "r" | "run" if manual_run => InputEvent::Run,
+ _ => InputEvent::Unrecognized(input.clone()),
+ };
+
+ if tx.send(WatchEvent::Input(input_event)).is_err() {
+ return;
+ }
+
+ input.clear();
+ }
+ KeyCode::Char(c) => {
+ input.push(c);
+ }
+ _ => (),
+ }
+ }
+ Event::Resize(_, _) => {
+ if tx.send(WatchEvent::TerminalResize).is_err() {
+ return;
+ }
+ }
+ Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue,
+ }
+ };
+
+ let _ = tx.send(WatchEvent::Input(last_input_event));
+}
diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs
index 7f1771b..81f48b1 100644
--- a/tests/dev_cargo_bins.rs
+++ b/tests/dev_cargo_bins.rs
@@ -1,38 +1,43 @@
// Makes sure that `dev/Cargo.toml` is synced with `info.toml`.
-// When this test fails, you just need to run `cargo run --bin gen-dev-cargo-toml`.
+// When this test fails, you just need to run `cargo run -p gen-dev-cargo-toml`.
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
-struct Exercise {
+struct ExerciseInfo {
name: String,
- path: String,
+ dir: Option<String>,
}
#[derive(Deserialize)]
-struct InfoToml {
- exercises: Vec<Exercise>,
+struct InfoFile {
+ exercises: Vec<ExerciseInfo>,
}
#[test]
fn dev_cargo_bins() {
- let content = fs::read_to_string("exercises/Cargo.toml").unwrap();
+ let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap();
- let exercises = toml_edit::de::from_str::<InfoToml>(&fs::read_to_string("info.toml").unwrap())
- .unwrap()
- .exercises;
+ let exercise_infos =
+ toml_edit::de::from_str::<InfoFile>(&fs::read_to_string("info.toml").unwrap())
+ .unwrap()
+ .exercises;
let mut start_ind = 0;
- for exercise in exercises {
- let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1;
- let name_end = name_start + content[name_start..].find('"').unwrap();
- assert_eq!(exercise.name, &content[name_start..name_end]);
-
- // +3 to skip `../` at the begeinning of the path.
- let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5;
- let path_end = path_start + content[path_start..].find('"').unwrap();
- assert_eq!(exercise.path, &content[path_start..path_end]);
+ for exercise_info in exercise_infos {
+ let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1;
+ let name_end = name_start + cargo_toml[name_start..].find('"').unwrap();
+ assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]);
+
+ let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2;
+ let path_end = path_start + cargo_toml[path_start..].find('"').unwrap();
+ let expected_path = if let Some(dir) = exercise_info.dir {
+ format!("../exercises/{dir}/{}.rs", exercise_info.name)
+ } else {
+ format!("../exercises/{}.rs", exercise_info.name)
+ };
+ assert_eq!(expected_path, &cargo_toml[path_start..path_end]);
start_ind = path_end + 1;
}
diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml
index e111cf2..7ee2f06 100644
--- a/tests/fixture/failure/Cargo.toml
+++ b/tests/fixture/failure/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "tests"
+name = "failure"
edition = "2021"
publish = false
diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml
index 9474ee3..94ec6ea 100644
--- a/tests/fixture/failure/info.toml
+++ b/tests/fixture/failure/info.toml
@@ -1,11 +1,9 @@
[[exercises]]
name = "compFailure"
-path = "exercises/compFailure.rs"
-mode = "compile"
+mode = "run"
hint = ""
[[exercises]]
name = "testFailure"
-path = "exercises/testFailure.rs"
mode = "test"
hint = "Hello!"
diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml
index c8d74e4..adbd8ab 100644
--- a/tests/fixture/state/Cargo.toml
+++ b/tests/fixture/state/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "tests"
+name = "state"
edition = "2021"
publish = false
diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs
index f579d0b..016b827 100644
--- a/tests/fixture/state/exercises/pending_exercise.rs
+++ b/tests/fixture/state/exercises/pending_exercise.rs
@@ -1,7 +1,5 @@
// fake_exercise
-// I AM NOT DONE
-
fn main() {
}
diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs
index 8756f02..2002ef1 100644
--- a/tests/fixture/state/exercises/pending_test_exercise.rs
+++ b/tests/fixture/state/exercises/pending_test_exercise.rs
@@ -1,4 +1,2 @@
-// I AM NOT DONE
-
#[test]
fn it_works() {}
diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml
index 8de5d60..e5c4d8f 100644
--- a/tests/fixture/state/info.toml
+++ b/tests/fixture/state/info.toml
@@ -1,17 +1,14 @@
[[exercises]]
name = "pending_exercise"
-path = "exercises/pending_exercise.rs"
-mode = "compile"
+mode = "run"
hint = """"""
[[exercises]]
name = "pending_test_exercise"
-path = "exercises/pending_test_exercise.rs"
mode = "test"
hint = """"""
[[exercises]]
name = "finished_exercise"
-path = "exercises/finished_exercise.rs"
-mode = "compile"
+mode = "run"
hint = """"""
diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml
index f26a44f..028cf35 100644
--- a/tests/fixture/success/Cargo.toml
+++ b/tests/fixture/success/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "tests"
+name = "success"
edition = "2021"
publish = false
diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml
index 17ed8c6..674ba26 100644
--- a/tests/fixture/success/info.toml
+++ b/tests/fixture/success/info.toml
@@ -1,11 +1,9 @@
[[exercises]]
name = "compSuccess"
-path = "exercises/compSuccess.rs"
-mode = "compile"
+mode = "run"
hint = """"""
[[exercises]]
name = "testSuccess"
-path = "exercises/testSuccess.rs"
mode = "test"
hint = """"""
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index d1694a3..f81cc94 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1,17 +1,8 @@
use assert_cmd::prelude::*;
-use glob::glob;
use predicates::boolean::PredicateBooleanExt;
-use std::fs::File;
-use std::io::Read;
use std::process::Command;
#[test]
-fn runs_without_arguments() {
- let mut cmd = Command::cargo_bin("rustlings").unwrap();
- cmd.assert().success();
-}
-
-#[test]
fn fails_when_in_wrong_dir() {
Command::cargo_bin("rustlings")
.unwrap()
@@ -21,26 +12,6 @@ fn fails_when_in_wrong_dir() {
}
#[test]
-fn verify_all_success() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .arg("verify")
- .current_dir("tests/fixture/success")
- .assert()
- .success();
-}
-
-#[test]
-fn verify_fails_if_some_fails() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .arg("verify")
- .current_dir("tests/fixture/failure")
- .assert()
- .code(1);
-}
-
-#[test]
fn run_single_compile_success() {
Command::cargo_bin("rustlings")
.unwrap()
@@ -91,19 +62,6 @@ fn run_single_test_not_passed() {
}
#[test]
-fn run_single_test_no_filename() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .arg("run")
- .current_dir("tests/fixture/")
- .assert()
- .code(2)
- .stderr(predicates::str::contains(
- "required arguments were not provided",
- ));
-}
-
-#[test]
fn run_single_test_no_exercise() {
Command::cargo_bin("rustlings")
.unwrap()
@@ -146,31 +104,6 @@ fn get_hint_for_single_test() {
}
#[test]
-fn all_exercises_require_confirmation() {
- for exercise in glob("exercises/**/*.rs").unwrap() {
- let path = exercise.unwrap();
- if path.file_name().unwrap() == "mod.rs" {
- continue;
- }
- let source = {
- let mut file = File::open(&path).unwrap();
- let mut s = String::new();
- file.read_to_string(&mut s).unwrap();
- s
- };
- source
- .matches("// I AM NOT DONE")
- .next()
- .unwrap_or_else(|| {
- panic!(
- "There should be an `I AM NOT DONE` annotation in {:?}",
- path
- )
- });
- }
-}
-
-#[test]
fn run_compile_exercise_does_not_prompt() {
Command::cargo_bin("rustlings")
.unwrap()
@@ -196,74 +129,9 @@ fn run_test_exercise_does_not_prompt() {
fn run_single_test_success_with_output() {
Command::cargo_bin("rustlings")
.unwrap()
- .args(["--nocapture", "run", "testSuccess"])
- .current_dir("tests/fixture/success/")
- .assert()
- .code(0)
- .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
-}
-
-#[test]
-fn run_single_test_success_without_output() {
- Command::cargo_bin("rustlings")
- .unwrap()
.args(["run", "testSuccess"])
.current_dir("tests/fixture/success/")
.assert()
.code(0)
- .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS").not());
-}
-
-#[test]
-fn run_rustlings_list() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .args(["list"])
- .current_dir("tests/fixture/success")
- .assert()
- .success();
-}
-
-#[test]
-fn run_rustlings_list_no_pending() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .args(["list"])
- .current_dir("tests/fixture/success")
- .assert()
- .success()
- .stdout(predicates::str::contains("Pending").not());
-}
-
-#[test]
-fn run_rustlings_list_both_done_and_pending() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .args(["list"])
- .current_dir("tests/fixture/state")
- .assert()
- .success()
- .stdout(predicates::str::contains("Done").and(predicates::str::contains("Pending")));
-}
-
-#[test]
-fn run_rustlings_list_without_pending() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .args(["list", "--solved"])
- .current_dir("tests/fixture/state")
- .assert()
- .success()
- .stdout(predicates::str::contains("Pending").not());
-}
-
-#[test]
-fn run_rustlings_list_without_done() {
- Command::cargo_bin("rustlings")
- .unwrap()
- .args(["list", "--unsolved"])
- .current_dir("tests/fixture/state")
- .assert()
- .success()
- .stdout(predicates::str::contains("Done").not());
+ .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS"));
}