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 -.DS_Store -*.pdb -.idea -.vscode/* -!.vscode/extensions.json -*.iml -*.o +# State file +.rustlings-state.txt + +# oranda public/ +.netlify + +# OS +.DS_Store .direnv/ -# Local Netlify folder -.netlify +# Editor +*.swp +.idea +*.iml + +# 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 @@ -2,6 +2,18 @@ # It is not intended for manual editing. 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" @@ -11,6 +23,12 @@ dependencies = [ "memchr", ] +[[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" @@ -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" @@ -109,6 +127,21 @@ dependencies = [ "serde", ] +[[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" @@ -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]] @@ -189,6 +222,31 @@ version = "0.8.19" 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" @@ -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]] @@ -364,12 +422,6 @@ dependencies = [ "libc", ] -[[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" @@ -382,6 +434,16 @@ version = "0.4.13" 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" @@ -389,10 +451,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "memchr" -version = "2.7.1" +name = "lru" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "portable-atomic" -version = "1.6.0" +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +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 = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "predicates" @@ -504,13 +598,33 @@ 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" @@ -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]] @@ -590,6 +701,12 @@ dependencies = [ "quote", ] +[[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" @@ -605,6 +722,12 @@ dependencies = [ "winapi-util", ] +[[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" @@ -622,18 +745,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" -dependencies = [ - "itoa", - "ryu", - "serde", + "syn 2.0.58", ] [[package]] @@ -646,22 +758,101 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "1.3.0" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "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 = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "2.0.55" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -702,6 +893,12 @@ version = "1.0.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" @@ -714,6 +911,12 @@ version = "0.2.1" 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" @@ -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 = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 . 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. + ## 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) { 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) -> Vec { 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 { 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 { 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 { 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(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::(); 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 similarity index 56% rename from src/bin/gen-dev-cargo-toml.rs rename to 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, } #[derive(Deserialize)] -struct InfoToml { - exercises: Vec, +struct InfoFile { + exercises: Vec, } fn main() -> Result<()> { - let exercises = toml_edit::de::from_str::( + let exercise_infos = toml_edit::de::from_str::( &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` 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` or `AsMut` 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, + n_done: u16, + final_message: String, + file_buf: Vec, +} + +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, + 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::>(); + + 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 { + 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 { + 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

(&self, path: P, strategy: WriteStrategy) -> io::Result<()> + where + P: AsRef, + { + 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 crossterm::style::{style, StyledContent, Stylize}; +use std::{ + fmt::{self, Display, Formatter}, + fs, + process::{Command, Output}, +}; -use crate::embedded::EMBEDDED_FILES; +use crate::{ + embedded::{WriteStrategy, EMBEDDED_FILES}, + info_file::Mode, +}; -// 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() +pub struct TerminalFileLink<'a> { + path: &'a str, } -// 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, -} - -#[derive(Deserialize)] -pub struct ExerciseList { - pub exercises: Vec, -} - -impl ExerciseList { - pub fn parse() -> Result { - // 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), -} - -// 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 { 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 { - 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 { - self.state().map(|state| state == State::Done) + pub fn terminal_link(&self) -> StyledContent> { + 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, + // 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, + pub final_message: Option, + pub exercises: Vec, +} + +impl InfoFile { + pub fn parse() -> Result { + // 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 one/

ending ā”‚ eset ā”‚ ontinue at ā”‚ 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 { + 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, + /// 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, }, /// 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, - /// 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(CARGO_NOT_FOUND_ERR)?; - which::which("cargo").context( - "Failed to find `cargo`. -Did you already install Rust? -Try running `cargo --version` to diagnose the problem.", - )?; - - 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); - }); + let (mut app_state, state_file_status) = AppState::new( + info_file.exercises, + info_file.final_message.unwrap_or_default(), + ); - 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"); + 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))?; + + let welcome_message = welcome_message.trim(); + write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + stdout.flush()?; + + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + + stdout.execute(Clear(ClearType::All))?; } - 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::>(); + StateFileStatus::Read => (), + } + } - 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), - }; - }); - } + match args.command { + None => { + let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + None + } else { + // 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::>() + .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)?, } } - - 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); } - - Subcommands::Run { name } => { - let exercise = find_exercise(&name, &exercises)?; - run(exercise, verbose).unwrap_or_else(|_| exit(1)); - } - - 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()); - } - - 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"), - }, - - 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); + // `Init` is handled above. + Some(Subcommands::Init) => (), + Some(Subcommands::Run { name }) => { + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; } - 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"); - } - }, + run(&mut app_state)?; + } + 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())?; + } + Some(Subcommands::Hint { name }) => { + app_state.set_current_exercise_by_name(&name)?; + println!("{}", app_state.current_exercise().hint); + } } Ok(()) } -fn spawn_watch_shell( - failed_exercise_hint: Arc>>, - should_quit: Arc, -) { - println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); +const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; - 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}"); - } - } else { - println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); - } - } - }); -} - -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); - } - } - - 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 { - /* 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:?}"), - } - // Check if we need to exit - if should_quit.load(Ordering::SeqCst) { - return Ok(WatchStatus::Unfinished); - } - } -} - -const WELCOME: &str = r" welcome to... +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 - ! - 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 { + 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> { + 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 crossterm::style::Stylize; +use std::io::{self, Write}; -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), - } -} - -// 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, - progress: (usize, usize), - verbose: bool, - success_hints: bool, -) -> Result> { - 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 { - 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 { - 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 { - 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, - success_hints: bool, -) -> Result { - 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 { + 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, + 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>, + stderr: Option>, + 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 { + 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, 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, } #[derive(Deserialize)] -struct InfoToml { - exercises: Vec, +struct InfoFile { + exercises: Vec, } #[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::(&fs::read_to_string("info.toml").unwrap()) - .unwrap() - .exercises; + let exercise_infos = + toml_edit::de::from_str::(&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]); + 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]); - // +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]); + 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,16 +1,7 @@ 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") @@ -20,26 +11,6 @@ fn fails_when_in_wrong_dir() { .code(1); } -#[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") @@ -90,19 +61,6 @@ fn run_single_test_not_passed() { .code(1); } -#[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") @@ -145,31 +103,6 @@ fn get_hint_for_single_test() { .stdout("Hello!\n"); } -#[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") @@ -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"]) + .args(["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()); -}