mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-01-09 20:03:24 +03:00
Compare commits
24 commits
d9df809838
...
cf3f6fd6a1
Author | SHA1 | Date | |
---|---|---|---|
cf3f6fd6a1 | |||
c8481d35c1 | |||
96a44f3dcf | |||
0ae66d1860 | |||
700605ff35 | |||
a67e63cce0 | |||
d48e86b154 | |||
39a19f9450 | |||
2dfc7cdb1a | |||
0add5ac240 | |||
5a1d95028c | |||
e80e91faf2 | |||
4ae3fcc3ca | |||
17a2d42ffd | |||
a7bc6d53a5 | |||
56eb4a5d65 | |||
f6cf6c611c | |||
7a74a72dc8 | |||
a4da216a5c | |||
8b2d9ed503 | |||
d2b5906be2 | |||
f9e35a4344 | |||
0525739046 | |||
11fda5d70f |
|
@ -37,7 +37,7 @@ Please be patient 😇
|
||||||
- Name the file `exercises/yourTopic/yourTopicN.rs`.
|
- Name the file `exercises/yourTopic/yourTopicN.rs`.
|
||||||
- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`.
|
- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`.
|
||||||
- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it.
|
- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it.
|
||||||
- Add the [metadata for your exercise](#exercise-metadata) in the `info.toml` file.
|
- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file.
|
||||||
- Make sure your exercise runs with `rustlings run yourTopicN`.
|
- Make sure your exercise runs with `rustlings run yourTopicN`.
|
||||||
- [Open a pull request](#pull-requests).
|
- [Open a pull request](#pull-requests).
|
||||||
|
|
||||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustlings"
|
name = "rustlings"
|
||||||
version = "6.0.0-beta.6"
|
version = "6.0.0-beta.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
|
@ -675,7 +675,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustlings-macros"
|
name = "rustlings-macros"
|
||||||
version = "6.0.0-beta.6"
|
version = "6.0.0-beta.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -8,7 +8,7 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "6.0.0-beta.6"
|
version = "6.0.0-beta.9"
|
||||||
authors = [
|
authors = [
|
||||||
"Liv <mokou@fastmail.com>",
|
"Liv <mokou@fastmail.com>",
|
||||||
"Mo Bitar <mo8it@proton.me>",
|
"Mo Bitar <mo8it@proton.me>",
|
||||||
|
@ -39,7 +39,6 @@ include = [
|
||||||
"/src/",
|
"/src/",
|
||||||
"/exercises/",
|
"/exercises/",
|
||||||
"/solutions/",
|
"/solutions/",
|
||||||
"/info.toml",
|
|
||||||
# A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded.
|
# A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded.
|
||||||
"/dev-Cargo.toml",
|
"/dev-Cargo.toml",
|
||||||
"/README.md",
|
"/README.md",
|
||||||
|
@ -54,7 +53,7 @@ hashbrown = "0.14.5"
|
||||||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||||
os_pipe = "1.1.5"
|
os_pipe = "1.1.5"
|
||||||
ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] }
|
ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] }
|
||||||
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" }
|
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" }
|
||||||
serde_json = "1.0.117"
|
serde_json = "1.0.117"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
toml_edit.workspace = true
|
toml_edit.workspace = true
|
||||||
|
|
|
@ -35,7 +35,7 @@ The following command will download and compile Rustlings:
|
||||||
<!-- TODO: Remove @6.0.0-beta.x -->
|
<!-- TODO: Remove @6.0.0-beta.x -->
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install rustlings@6.0.0-beta.6
|
cargo install rustlings@6.0.0-beta.9
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.6
|
||||||
<!-- TODO: Remove @6.0.0-beta.x -->
|
<!-- TODO: Remove @6.0.0-beta.x -->
|
||||||
|
|
||||||
- Make sure you have the latest Rust version by running `rustup update`
|
- Make sure you have the latest Rust version by running `rustup update`
|
||||||
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.6 --locked`
|
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked`
|
||||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -98,7 +98,7 @@ It will rerun the current exercise automatically every time you change the exerc
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
|
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
|
||||||
|
|
||||||
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode.
|
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
||||||
>
|
>
|
||||||
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
|
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ It will rerun the current exercise automatically every time you change the exerc
|
||||||
|
|
||||||
### Exercise List
|
### Exercise List
|
||||||
|
|
||||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list.
|
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
||||||
|
|
||||||
The list allows you to…
|
The list allows you to…
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// We sometimes encourage you to keep trying things on a given exercise, even
|
// 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
|
// after you already figured it out. If you got everything working and feel
|
||||||
// ready for the next exercise, enter `n` (or `next`) in the terminal.
|
// ready for the next exercise, enter `n` in the terminal.
|
||||||
//
|
//
|
||||||
// The exercise file will be reloaded when you change one of the lines below!
|
// The exercise file will be reloaded when you change one of the lines below!
|
||||||
// Try adding a new `println!`.
|
// Try adding a new `println!`.
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../info.toml
|
|
1286
rustlings-macros/info.toml
Normal file
1286
rustlings-macros/info.toml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,8 @@ struct InfoFile {
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||||
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml"))
|
let info_file = include_str!("../info.toml");
|
||||||
|
let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
|
||||||
.expect("Failed to parse `info.toml`")
|
.expect("Failed to parse `info.toml`")
|
||||||
.exercises;
|
.exercises;
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ pub fn include_files(_: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
EmbeddedFiles {
|
EmbeddedFiles {
|
||||||
|
info_file: #info_file,
|
||||||
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
|
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
|
||||||
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
|
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,12 @@ const BAD_INDEX_ERR: &str = "The current exercise index is higher than the numbe
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub enum ExercisesProgress {
|
pub enum ExercisesProgress {
|
||||||
|
// All exercises are done.
|
||||||
AllDone,
|
AllDone,
|
||||||
Pending,
|
// The current exercise failed and is still pending.
|
||||||
|
CurrentPending,
|
||||||
|
// A new exercise is now pending.
|
||||||
|
NewPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StateFileStatus {
|
pub enum StateFileStatus {
|
||||||
|
@ -120,7 +124,27 @@ impl AppState {
|
||||||
|
|
||||||
let exercises = exercise_infos
|
let exercises = exercise_infos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Exercise::from)
|
.map(|exercise_info| {
|
||||||
|
// Leaking to be able to borrow in the watch mode `Table`.
|
||||||
|
// Leaking is not a problem because the `AppState` instance lives until
|
||||||
|
// the end of the program.
|
||||||
|
let path = exercise_info.path().leak();
|
||||||
|
let name = exercise_info.name.leak();
|
||||||
|
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||||
|
|
||||||
|
let hint = exercise_info.hint.trim().to_owned();
|
||||||
|
|
||||||
|
Exercise {
|
||||||
|
dir,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
test: exercise_info.test,
|
||||||
|
strict_clippy: exercise_info.strict_clippy,
|
||||||
|
hint,
|
||||||
|
// Updated in `Self::update_from_file`.
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut slf = Self {
|
let mut slf = Self {
|
||||||
|
@ -194,6 +218,10 @@ impl AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
||||||
|
if exercise_ind == self.current_exercise_ind {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if exercise_ind >= self.exercises.len() {
|
if exercise_ind >= self.exercises.len() {
|
||||||
bail!(BAD_INDEX_ERR);
|
bail!(BAD_INDEX_ERR);
|
||||||
}
|
}
|
||||||
|
@ -302,8 +330,8 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Official exercises: Dump the solution file form the binary and return its path.
|
/// Official exercises: Dump the solution file form the binary and return its path.
|
||||||
// Third-party exercises: Check if a solution file exists and return its path in that case.
|
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||||
if DEBUG_PROFILE {
|
if DEBUG_PROFILE {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
@ -330,9 +358,9 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||||
// If all exercises are marked as done, run all of them to make sure that they are actually
|
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||||
// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||||
if !exercise.done {
|
if !exercise.done {
|
||||||
|
@ -343,7 +371,7 @@ impl AppState {
|
||||||
if let Some(ind) = self.next_pending_exercise_ind() {
|
if let Some(ind) = self.next_pending_exercise_ind() {
|
||||||
self.set_current_exercise_ind(ind)?;
|
self.set_current_exercise_ind(ind)?;
|
||||||
|
|
||||||
return Ok(ExercisesProgress::Pending);
|
return Ok(ExercisesProgress::NewPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
||||||
|
@ -366,7 +394,7 @@ impl AppState {
|
||||||
|
|
||||||
self.write()?;
|
self.write()?;
|
||||||
|
|
||||||
return Ok(ExercisesProgress::Pending);
|
return Ok(ExercisesProgress::NewPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(writer, "{}", "ok".green())?;
|
writeln!(writer, "{}", "ok".green())?;
|
||||||
|
|
|
@ -2,10 +2,10 @@ use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::info_file::ExerciseInfo;
|
||||||
|
|
||||||
// Return the start and end index of the content of the list `bin = […]`.
|
/// Return the start and end index of the content of the list `bin = […]`.
|
||||||
// bin = [xxxxxxxxxxxxxxxxx]
|
/// bin = [xxxxxxxxxxxxxxxxx]
|
||||||
// |start_ind |
|
/// |start_ind |
|
||||||
// |end_ind
|
/// |end_ind
|
||||||
pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
||||||
let start_ind = cargo_toml
|
let start_ind = cargo_toml
|
||||||
.find("bin = [")
|
.find("bin = [")
|
||||||
|
@ -20,8 +20,8 @@ pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
||||||
Ok((start_ind, end_ind))
|
Ok((start_ind, end_ind))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and append the content of the `bin` list in `Cargo.toml`.
|
/// Generate and append the content of the `bin` list in `Cargo.toml`.
|
||||||
// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
|
/// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
|
||||||
pub fn append_bins(
|
pub fn append_bins(
|
||||||
buf: &mut Vec<u8>,
|
buf: &mut Vec<u8>,
|
||||||
exercise_infos: &[ExerciseInfo],
|
exercise_infos: &[ExerciseInfo],
|
||||||
|
@ -43,7 +43,7 @@ pub fn append_bins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the `bin` list and leave everything else unchanged.
|
/// Update the `bin` list and leave everything else unchanged.
|
||||||
pub fn updated_cargo_toml(
|
pub fn updated_cargo_toml(
|
||||||
exercise_infos: &[ExerciseInfo],
|
exercise_infos: &[ExerciseInfo],
|
||||||
current_cargo_toml: &str,
|
current_cargo_toml: &str,
|
||||||
|
|
14
src/cmd.rs
14
src/cmd.rs
|
@ -1,8 +1,8 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::{io::Read, path::Path, process::Command};
|
use std::{io::Read, path::Path, process::Command};
|
||||||
|
|
||||||
// Run a command with a description for a possible error and append the merged stdout and stderr.
|
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||||
// The boolean in the returned `Result` is true if the command's exit status is success.
|
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||||
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
|
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
|
||||||
let (mut reader, writer) = os_pipe::pipe()
|
let (mut reader, writer) = os_pipe::pipe()
|
||||||
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
|
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
|
||||||
|
@ -37,18 +37,18 @@ pub struct CargoCmd<'a> {
|
||||||
pub args: &'a [&'a str],
|
pub args: &'a [&'a str],
|
||||||
pub exercise_name: &'a str,
|
pub exercise_name: &'a str,
|
||||||
pub description: &'a str,
|
pub description: &'a str,
|
||||||
// RUSTFLAGS="-A warnings"
|
/// RUSTFLAGS="-A warnings"
|
||||||
pub hide_warnings: bool,
|
pub hide_warnings: bool,
|
||||||
// Added as `--target-dir` if `Self::dev` is true.
|
/// Added as `--target-dir` if `Self::dev` is true.
|
||||||
pub target_dir: &'a Path,
|
pub target_dir: &'a Path,
|
||||||
// The output buffer to append the merged stdout and stderr.
|
/// The output buffer to append the merged stdout and stderr.
|
||||||
pub output: &'a mut Vec<u8>,
|
pub output: &'a mut Vec<u8>,
|
||||||
// true while developing Rustlings.
|
/// true while developing Rustlings.
|
||||||
pub dev: bool,
|
pub dev: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CargoCmd<'a> {
|
impl<'a> CargoCmd<'a> {
|
||||||
// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
||||||
pub fn run(&mut self) -> Result<bool> {
|
pub fn run(&mut self) -> Result<bool> {
|
||||||
let mut cmd = Command::new("cargo");
|
let mut cmd = Command::new("cargo");
|
||||||
cmd.arg(self.subcommand);
|
cmd.arg(self.subcommand);
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
|
|
||||||
use crate::info_file::ExerciseInfo;
|
use crate::info_file::ExerciseInfo;
|
||||||
|
|
||||||
|
/// Contains all embedded files.
|
||||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -31,12 +32,17 @@ impl WriteStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Files related to one exercise.
|
||||||
struct ExerciseFiles {
|
struct ExerciseFiles {
|
||||||
|
// The content of the exercise file.
|
||||||
exercise: &'static [u8],
|
exercise: &'static [u8],
|
||||||
|
// The content of the solution file.
|
||||||
solution: &'static [u8],
|
solution: &'static [u8],
|
||||||
|
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
|
||||||
dir_ind: usize,
|
dir_ind: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A directory in the `exercises/` directory.
|
||||||
struct ExerciseDir {
|
struct ExerciseDir {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
readme: &'static [u8],
|
readme: &'static [u8],
|
||||||
|
@ -63,18 +69,20 @@ impl ExerciseDir {
|
||||||
let mut readme_path = dir_path;
|
let mut readme_path = dir_path;
|
||||||
readme_path.push_str("/README.md");
|
readme_path.push_str("/README.md");
|
||||||
|
|
||||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)?;
|
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All embedded files.
|
||||||
pub struct EmbeddedFiles {
|
pub struct EmbeddedFiles {
|
||||||
|
/// The content of the `info.toml` file.
|
||||||
|
pub info_file: &'static str,
|
||||||
exercise_files: &'static [ExerciseFiles],
|
exercise_files: &'static [ExerciseFiles],
|
||||||
exercise_dirs: &'static [ExerciseDir],
|
exercise_dirs: &'static [ExerciseDir],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmbeddedFiles {
|
impl EmbeddedFiles {
|
||||||
|
/// Dump all the embedded files of the `exercises/` directory.
|
||||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||||
|
|
||||||
|
@ -95,21 +103,21 @@ impl EmbeddedFiles {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||||
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind];
|
let exercise_files = &self.exercise_files[exercise_ind];
|
||||||
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
dir.init_on_disk()?;
|
dir.init_on_disk()?;
|
||||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the solution file to disk and return its path.
|
/// Write the solution file to disk and return its path.
|
||||||
pub fn write_solution_to_disk(
|
pub fn write_solution_to_disk(
|
||||||
&self,
|
&self,
|
||||||
exercise_ind: usize,
|
exercise_ind: usize,
|
||||||
exercise_name: &str,
|
exercise_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind];
|
let exercise_files = &self.exercise_files[exercise_ind];
|
||||||
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind];
|
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||||
|
|
||||||
// 14 = 10 + 1 + 3
|
// 14 = 10 + 1 + 3
|
||||||
// solutions/ + / + .rs
|
// solutions/ + / + .rs
|
||||||
|
@ -148,7 +156,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dirs() {
|
fn dirs() {
|
||||||
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml"))
|
let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
|
||||||
.expect("Failed to parse `info.toml`")
|
.expect("Failed to parse `info.toml`")
|
||||||
.exercises;
|
.exercises;
|
||||||
|
|
||||||
|
|
|
@ -10,30 +10,32 @@ use std::{
|
||||||
use crate::{
|
use crate::{
|
||||||
cmd::{run_cmd, CargoCmd},
|
cmd::{run_cmd, CargoCmd},
|
||||||
in_official_repo,
|
in_official_repo,
|
||||||
info_file::ExerciseInfo,
|
|
||||||
terminal_link::TerminalFileLink,
|
terminal_link::TerminalFileLink,
|
||||||
DEBUG_PROFILE,
|
DEBUG_PROFILE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The initial capacity of the output buffer.
|
||||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||||
|
|
||||||
|
/// See `info_file::ExerciseInfo`
|
||||||
pub struct Exercise {
|
pub struct Exercise {
|
||||||
pub dir: Option<&'static str>,
|
pub dir: Option<&'static str>,
|
||||||
// Exercise's unique name
|
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
// Exercise's path
|
/// Path of the exercise file starting with the `exercises/` directory.
|
||||||
pub path: &'static str,
|
pub path: &'static str,
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
pub strict_clippy: bool,
|
pub strict_clippy: bool,
|
||||||
// The hint text associated with the exercise
|
|
||||||
pub hint: String,
|
pub hint: String,
|
||||||
pub done: bool,
|
pub done: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Exercise {
|
impl Exercise {
|
||||||
|
// Run the exercise's binary and append its output to the `output` buffer.
|
||||||
|
// Compilation should be done before calling this method.
|
||||||
fn run_bin(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
fn run_bin(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||||
writeln!(output, "{}", "Output".underlined())?;
|
writeln!(output, "{}", "Output".underlined())?;
|
||||||
|
|
||||||
|
// 7 = "/debug/".len()
|
||||||
let mut bin_path =
|
let mut bin_path =
|
||||||
PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len());
|
PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len());
|
||||||
bin_path.push(target_dir);
|
bin_path.push(target_dir);
|
||||||
|
@ -43,18 +45,23 @@ impl Exercise {
|
||||||
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
|
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
|
// This output is important to show the user that something went wrong.
|
||||||
|
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||||
|
// leaves the user confused about why the exercise isn't done yet.
|
||||||
writeln!(
|
writeln!(
|
||||||
output,
|
output,
|
||||||
"{}",
|
"{}",
|
||||||
"The exercise didn't run successfully (nonzero exit code)"
|
"The exercise didn't run successfully (nonzero exit code)"
|
||||||
.bold()
|
.bold()
|
||||||
.red()
|
.red(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(success)
|
Ok(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile, check and run the exercise.
|
||||||
|
/// The output is written to the `output` buffer after clearing it.
|
||||||
pub fn run(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
pub fn run(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||||
output.clear();
|
output.clear();
|
||||||
|
|
||||||
|
@ -76,9 +83,10 @@ impl Exercise {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard the output of `cargo build` because it will be shown again by the Cargo command.
|
// Discard the output of `cargo build` because it will be shown again by Clippy.
|
||||||
output.clear();
|
output.clear();
|
||||||
|
|
||||||
|
// `--profile test` is required to also check code with `[cfg(test)]`.
|
||||||
let clippy_args: &[&str] = if self.strict_clippy {
|
let clippy_args: &[&str] = if self.strict_clippy {
|
||||||
&["--profile", "test", "--", "-D", "warnings"]
|
&["--profile", "test", "--", "-D", "warnings"]
|
||||||
} else {
|
} else {
|
||||||
|
@ -126,34 +134,6 @@ impl Exercise {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ExerciseInfo> for Exercise {
|
|
||||||
fn from(mut exercise_info: ExerciseInfo) -> Self {
|
|
||||||
// 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 dir = exercise_info.dir.map(|mut dir| {
|
|
||||||
dir.shrink_to_fit();
|
|
||||||
&*dir.leak()
|
|
||||||
});
|
|
||||||
|
|
||||||
let hint = exercise_info.hint.trim().to_owned();
|
|
||||||
|
|
||||||
Exercise {
|
|
||||||
dir,
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
test: exercise_info.test,
|
|
||||||
strict_clippy: exercise_info.strict_clippy,
|
|
||||||
hint,
|
|
||||||
done: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Exercise {
|
impl Display for Exercise {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
self.path.fmt(f)
|
self.path.fmt(f)
|
||||||
|
|
|
@ -2,44 +2,71 @@ use anyhow::{bail, Context, Error, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fs, io::ErrorKind};
|
use std::{fs, io::ErrorKind};
|
||||||
|
|
||||||
// Deserialized from the `info.toml` file.
|
use crate::embedded::EMBEDDED_FILES;
|
||||||
|
|
||||||
|
/// Deserialized from the `info.toml` file.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ExerciseInfo {
|
pub struct ExerciseInfo {
|
||||||
// Name of the exercise
|
/// Exercise's unique name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
// The exercise's directory inside the `exercises` directory
|
/// Exercise's directory name inside the `exercises/` directory.
|
||||||
pub dir: Option<String>,
|
pub dir: Option<String>,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
|
/// Run `cargo test` on the exercise.
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
|
/// Deny all Clippy warnings.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub strict_clippy: bool,
|
pub strict_clippy: bool,
|
||||||
// The hint text associated with the exercise
|
/// The exercise's hint to be shown to the user on request.
|
||||||
pub hint: String,
|
pub hint: String,
|
||||||
}
|
}
|
||||||
#[inline]
|
#[inline(always)]
|
||||||
const fn default_true() -> bool {
|
const fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExerciseInfo {
|
impl ExerciseInfo {
|
||||||
|
/// Path to the exercise file starting with the `exercises/` directory.
|
||||||
pub fn path(&self) -> String {
|
pub fn path(&self) -> String {
|
||||||
if let Some(dir) = &self.dir {
|
let mut path = if let Some(dir) = &self.dir {
|
||||||
format!("exercises/{dir}/{}.rs", self.name)
|
// 14 = 10 + 1 + 3
|
||||||
|
// exercises/ + / + .rs
|
||||||
|
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
|
||||||
|
path.push_str("exercises/");
|
||||||
|
path.push_str(dir);
|
||||||
|
path.push('/');
|
||||||
|
path
|
||||||
} else {
|
} else {
|
||||||
format!("exercises/{}.rs", self.name)
|
// 13 = 10 + 3
|
||||||
}
|
// exercises/ + .rs
|
||||||
|
let mut path = String::with_capacity(13 + self.name.len());
|
||||||
|
path.push_str("exercises/");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
path.push_str(&self.name);
|
||||||
|
path.push_str(".rs");
|
||||||
|
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The deserialized `info.toml` file.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct InfoFile {
|
pub struct InfoFile {
|
||||||
|
/// For possible breaking changes in the future for third-party exercises.
|
||||||
pub format_version: u8,
|
pub format_version: u8,
|
||||||
|
/// Shown to users when starting with the exercises.
|
||||||
pub welcome_message: Option<String>,
|
pub welcome_message: Option<String>,
|
||||||
|
/// Shown to users after finishing all exercises.
|
||||||
pub final_message: Option<String>,
|
pub final_message: Option<String>,
|
||||||
|
/// List of all exercises.
|
||||||
pub exercises: Vec<ExerciseInfo>,
|
pub exercises: Vec<ExerciseInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfoFile {
|
impl InfoFile {
|
||||||
|
/// Official exercises: Parse the embedded `info.toml` file.
|
||||||
|
/// Third-party exercises: Parse the `info.toml` file in the current directory.
|
||||||
pub fn parse() -> Result<Self> {
|
pub fn parse() -> Result<Self> {
|
||||||
// Read a local `info.toml` if it exists.
|
// Read a local `info.toml` if it exists.
|
||||||
let slf = match fs::read_to_string("info.toml") {
|
let slf = match fs::read_to_string("info.toml") {
|
||||||
|
@ -47,7 +74,7 @@ impl InfoFile {
|
||||||
.context("Failed to parse the `info.toml` file")?,
|
.context("Failed to parse the `info.toml` file")?,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() == ErrorKind::NotFound {
|
if e.kind() == ErrorKind::NotFound {
|
||||||
return toml_edit::de::from_str(include_str!("../info.toml"))
|
return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
|
||||||
.context("Failed to parse the embedded `info.toml` file");
|
.context("Failed to parse the embedded `info.toml` file");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
24
src/init.rs
24
src/init.rs
|
@ -11,8 +11,11 @@ use std::{
|
||||||
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
|
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
|
||||||
|
|
||||||
pub fn init() -> Result<()> {
|
pub fn init() -> Result<()> {
|
||||||
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
|
// Prevent initialization in a directory that contains the file `Cargo.toml`.
|
||||||
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
|
// This can mean that Rustlings was already initialized in this directory.
|
||||||
|
// Otherwise, this can cause problems with Cargo workspaces.
|
||||||
|
if Path::new("Cargo.toml").exists() {
|
||||||
|
bail!(CARGO_TOML_EXISTS_ERR);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rustlings_path = Path::new("rustlings");
|
let rustlings_path = Path::new("rustlings");
|
||||||
|
@ -24,7 +27,7 @@ pub fn init() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
set_current_dir("rustlings")
|
set_current_dir("rustlings")
|
||||||
.context("Failed to change the current directory to `rustlings`")?;
|
.context("Failed to change the current directory to `rustlings/`")?;
|
||||||
|
|
||||||
let info_file = InfoFile::parse()?;
|
let info_file = InfoFile::parse()?;
|
||||||
EMBEDDED_FILES
|
EMBEDDED_FILES
|
||||||
|
@ -37,9 +40,10 @@ pub fn init() -> Result<()> {
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.iter()
|
.iter()
|
||||||
.position(|c| *c == b'\n')
|
.position(|c| *c == b'\n')
|
||||||
.context("The embedded `Cargo.toml` is empty or contains only one line.")?;
|
.context("The embedded `Cargo.toml` is empty or contains only one line")?;
|
||||||
let current_cargo_toml =
|
let current_cargo_toml = current_cargo_toml
|
||||||
¤t_cargo_toml[(newline_ind + 1).min(current_cargo_toml.len() - 1)..];
|
.get(newline_ind + 1..)
|
||||||
|
.context("The embedded `Cargo.toml` contains only one line")?;
|
||||||
let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
|
let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
|
||||||
.context("Failed to generate `Cargo.toml`")?;
|
.context("Failed to generate `Cargo.toml`")?;
|
||||||
fs::write("Cargo.toml", updated_cargo_toml)
|
fs::write("Cargo.toml", updated_cargo_toml)
|
||||||
|
@ -77,12 +81,10 @@ target
|
||||||
|
|
||||||
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
||||||
|
|
||||||
const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str =
|
const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
|
||||||
"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.";
|
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
|
||||||
|
Otherwise, please run `rustlings init` again in another directory.";
|
||||||
|
|
||||||
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
||||||
"A directory with the name `rustlings` already exists in the current directory.
|
"A directory with the name `rustlings` already exists in the current directory.
|
||||||
|
|
|
@ -148,14 +148,12 @@ impl<'a> UiState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn select_first(&mut self) {
|
pub fn select_first(&mut self) {
|
||||||
if self.n_rows > 0 {
|
if self.n_rows > 0 {
|
||||||
self.table_state.select(Some(0));
|
self.table_state.select(Some(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn select_last(&mut self) {
|
pub fn select_last(&mut self) {
|
||||||
if self.n_rows > 0 {
|
if self.n_rows > 0 {
|
||||||
self.table_state.select(Some(self.n_rows - 1));
|
self.table_state.select(Some(self.n_rows - 1));
|
||||||
|
|
|
@ -56,7 +56,7 @@ fn press_enter_prompt() -> io::Result<()> {
|
||||||
struct Args {
|
struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Subcommands>,
|
command: Option<Subcommands>,
|
||||||
/// Manually run the current exercise using `r` or `run` in the watch mode.
|
/// Manually run the current exercise using `r` in the watch mode.
|
||||||
/// Only use this if Rustlings fails to detect exercise file changes.
|
/// Only use this if Rustlings fails to detect exercise file changes.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
manual_run: bool,
|
manual_run: bool,
|
||||||
|
|
|
@ -12,6 +12,7 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
||||||
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
||||||
"The progress of the progress bar is higher than the maximum";
|
"The progress of the progress bar is higher than the maximum";
|
||||||
|
|
||||||
|
/// Terminal progress bar to be used when not using Ratataui.
|
||||||
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
|
||||||
Ok(line)
|
Ok(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Progress bar to be used with Ratataui.
|
||||||
|
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
|
||||||
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
||||||
|
|
||||||
match app_state.done_current_exercise(&mut stdout)? {
|
match app_state.done_current_exercise(&mut stdout)? {
|
||||||
ExercisesProgress::AllDone => (),
|
ExercisesProgress::AllDone => (),
|
||||||
ExercisesProgress::Pending => println!(
|
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
|
||||||
"Next exercise: {}",
|
"Next exercise: {}",
|
||||||
app_state.current_exercise().terminal_link(),
|
app_state.current_exercise().terminal_link(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,15 +7,18 @@ pub struct TerminalFileLink<'a>(pub &'a str);
|
||||||
|
|
||||||
impl<'a> Display for TerminalFileLink<'a> {
|
impl<'a> Display for TerminalFileLink<'a> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
if let Ok(Some(canonical_path)) = fs::canonicalize(self.0)
|
let path = fs::canonicalize(self.0);
|
||||||
.as_deref()
|
|
||||||
.map(|path| path.to_str())
|
if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
|
||||||
{
|
// Windows itself can't handle its verbatim paths.
|
||||||
write!(
|
#[cfg(windows)]
|
||||||
f,
|
let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
|
||||||
"\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
|
&path[4..]
|
||||||
canonical_path, self.0,
|
} else {
|
||||||
)
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
}
|
}
|
||||||
|
|
16
src/watch.rs
16
src/watch.rs
|
@ -14,7 +14,7 @@ use std::{
|
||||||
use crate::app_state::{AppState, ExercisesProgress};
|
use crate::app_state::{AppState, ExercisesProgress};
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
notify_event::DebounceEventHandler,
|
notify_event::NotifyEventHandler,
|
||||||
state::WatchState,
|
state::WatchState,
|
||||||
terminal_event::{terminal_event_handler, InputEvent},
|
terminal_event::{terminal_event_handler, InputEvent},
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,7 @@ pub enum WatchExit {
|
||||||
List,
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `notify_exercise_names` as None activates the manual run mode.
|
||||||
pub fn watch(
|
pub fn watch(
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
|
@ -52,7 +53,7 @@ pub fn watch(
|
||||||
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||||
let mut debouncer = new_debouncer(
|
let mut debouncer = new_debouncer(
|
||||||
Duration::from_millis(200),
|
Duration::from_millis(200),
|
||||||
DebounceEventHandler {
|
NotifyEventHandler {
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
exercise_names,
|
exercise_names,
|
||||||
},
|
},
|
||||||
|
@ -79,7 +80,8 @@ pub fn watch(
|
||||||
match event {
|
match event {
|
||||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
|
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
|
||||||
ExercisesProgress::AllDone => break,
|
ExercisesProgress::AllDone => break,
|
||||||
ExercisesProgress::Pending => watch_state.run_current_exercise()?,
|
ExercisesProgress::CurrentPending => watch_state.render()?,
|
||||||
|
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
|
||||||
},
|
},
|
||||||
WatchEvent::Input(InputEvent::Hint) => {
|
WatchEvent::Input(InputEvent::Hint) => {
|
||||||
watch_state.show_hint()?;
|
watch_state.show_hint()?;
|
||||||
|
@ -92,11 +94,9 @@ pub fn watch(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
|
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
|
||||||
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
|
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
|
||||||
watch_state.handle_invalid_cmd(&cmd)?;
|
|
||||||
}
|
|
||||||
WatchEvent::FileChange { exercise_ind } => {
|
WatchEvent::FileChange { exercise_ind } => {
|
||||||
watch_state.run_exercise_with_ind(exercise_ind)?;
|
watch_state.handle_file_change(exercise_ind)?;
|
||||||
}
|
}
|
||||||
WatchEvent::TerminalResize => {
|
WatchEvent::TerminalResize => {
|
||||||
watch_state.render()?;
|
watch_state.render()?;
|
||||||
|
@ -124,5 +124,5 @@ The automatic detection of exercise file changes failed :(
|
||||||
Please try running `rustlings` again.
|
Please try running `rustlings` again.
|
||||||
|
|
||||||
If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
|
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.
|
You need to manually trigger running the current exercise using `r` then.
|
||||||
";
|
";
|
||||||
|
|
|
@ -3,23 +3,24 @@ use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
use super::WatchEvent;
|
use super::WatchEvent;
|
||||||
|
|
||||||
pub struct DebounceEventHandler {
|
pub struct NotifyEventHandler {
|
||||||
pub tx: Sender<WatchEvent>,
|
pub tx: Sender<WatchEvent>,
|
||||||
|
/// Used to report which exercise was modified.
|
||||||
pub exercise_names: &'static [&'static [u8]],
|
pub exercise_names: &'static [&'static [u8]],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
|
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
||||||
fn handle_event(&mut self, event: DebounceEventResult) {
|
fn handle_event(&mut self, input_event: DebounceEventResult) {
|
||||||
let event = match event {
|
let output_event = match input_event {
|
||||||
Ok(event) => {
|
Ok(input_event) => {
|
||||||
let Some(exercise_ind) = event
|
let Some(exercise_ind) = input_event
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|event| {
|
.filter_map(|input_event| {
|
||||||
if event.kind != DebouncedEventKind::Any {
|
if input_event.kind != DebouncedEventKind::Any {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_name = event.path.file_name()?.to_str()?.as_bytes();
|
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
|
||||||
|
|
||||||
if file_name.len() < 4 {
|
if file_name.len() < 4 {
|
||||||
return None;
|
return None;
|
||||||
|
@ -46,6 +47,6 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
|
||||||
|
|
||||||
// An error occurs when the receiver is dropped.
|
// An error occurs when the receiver is dropped.
|
||||||
// After dropping the receiver, the debouncer guard should also be dropped.
|
// After dropping the receiver, the debouncer guard should also be dropped.
|
||||||
let _ = self.tx.send(event);
|
let _ = self.tx.send(output_event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
style::{style, Stylize},
|
style::{style, Stylize},
|
||||||
terminal::size,
|
terminal,
|
||||||
};
|
};
|
||||||
use std::io::{self, StdoutLock, Write};
|
use std::io::{self, StdoutLock, Write};
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
terminal_link::TerminalFileLink,
|
terminal_link::TerminalFileLink,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
enum DoneStatus {
|
enum DoneStatus {
|
||||||
DoneWithSolution(String),
|
DoneWithSolution(String),
|
||||||
DoneWithoutSolution,
|
DoneWithoutSolution,
|
||||||
|
@ -71,17 +72,22 @@ impl<'a> WatchState<'a> {
|
||||||
self.render()
|
self.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
|
||||||
|
// Don't skip exercises on file changes to avoid confusion from missing exercises.
|
||||||
|
// Skipping exercises must be explicit in the interactive list.
|
||||||
|
// But going back to an earlier exercise on file change is fine.
|
||||||
|
if self.app_state.current_exercise_ind() < exercise_ind {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||||
self.run_current_exercise()
|
self.run_current_exercise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move on to the next exercise if the current one is done.
|
||||||
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
|
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
|
||||||
if matches!(self.done_status, DoneStatus::Pending) {
|
if self.done_status == DoneStatus::Pending {
|
||||||
self.writer
|
return Ok(ExercisesProgress::CurrentPending);
|
||||||
.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)
|
self.app_state.done_current_exercise(&mut self.writer)
|
||||||
|
@ -91,24 +97,24 @@ impl<'a> WatchState<'a> {
|
||||||
self.writer.write_all(b"\n")?;
|
self.writer.write_all(b"\n")?;
|
||||||
|
|
||||||
if self.manual_run {
|
if self.manual_run {
|
||||||
write!(self.writer, "{}un/", 'r'.bold())?;
|
write!(self.writer, "{}:run / ", 'r'.bold())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(self.done_status, DoneStatus::Pending) {
|
if self.done_status != DoneStatus::Pending {
|
||||||
write!(self.writer, "{}ext/", 'n'.bold())?;
|
write!(self.writer, "{}:next / ", 'n'.bold())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.show_hint {
|
if !self.show_hint {
|
||||||
write!(self.writer, "{}int/", 'h'.bold())?;
|
write!(self.writer, "{}:hint / ", 'h'.bold())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?;
|
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
|
||||||
|
|
||||||
self.writer.flush()
|
self.writer.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<()> {
|
pub fn render(&mut self) -> Result<()> {
|
||||||
// Prevent having the first line shifted.
|
// Prevent having the first line shifted if clearing wasn't successful.
|
||||||
self.writer.write_all(b"\n")?;
|
self.writer.write_all(b"\n")?;
|
||||||
|
|
||||||
clear_terminal(&mut self.writer)?;
|
clear_terminal(&mut self.writer)?;
|
||||||
|
@ -125,12 +131,12 @@ impl<'a> WatchState<'a> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(self.done_status, DoneStatus::Pending) {
|
if self.done_status != DoneStatus::Pending {
|
||||||
writeln!(
|
writeln!(
|
||||||
self.writer,
|
self.writer,
|
||||||
"{}\n",
|
"{}\n",
|
||||||
"Exercise done ✓
|
"Exercise done ✓
|
||||||
When you are done experimenting, enter `n` (or `next`) to move on to the next exercise 🦀"
|
When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
||||||
.bold()
|
.bold()
|
||||||
.green(),
|
.green(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -140,11 +146,11 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex
|
||||||
writeln!(
|
writeln!(
|
||||||
self.writer,
|
self.writer,
|
||||||
"A solution file can be found at {}\n",
|
"A solution file can be found at {}\n",
|
||||||
style(TerminalFileLink(solution_path)).underlined().green()
|
style(TerminalFileLink(solution_path)).underlined().green(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_width = size()?.0;
|
let line_width = terminal::size()?.0;
|
||||||
let progress_bar = progress_bar(
|
let progress_bar = progress_bar(
|
||||||
self.app_state.n_done(),
|
self.app_state.n_done(),
|
||||||
self.app_state.exercises().len() as u16,
|
self.app_state.exercises().len() as u16,
|
||||||
|
@ -165,15 +171,4 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex
|
||||||
self.show_hint = true;
|
self.show_hint = true;
|
||||||
self.render()
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,12 @@ pub enum InputEvent {
|
||||||
Hint,
|
Hint,
|
||||||
List,
|
List,
|
||||||
Quit,
|
Quit,
|
||||||
Unrecognized(String),
|
Unrecognized,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||||
let mut input = String::with_capacity(8);
|
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
||||||
|
let mut last_input_valid = false;
|
||||||
|
|
||||||
let last_input_event = loop {
|
let last_input_event = loop {
|
||||||
let terminal_event = match event::read() {
|
let terminal_event = match event::read() {
|
||||||
|
@ -28,37 +29,49 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||||
|
|
||||||
match terminal_event {
|
match terminal_event {
|
||||||
Event::Key(key) => {
|
Event::Key(key) => {
|
||||||
|
match key.kind {
|
||||||
|
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
||||||
|
KeyEventKind::Press => (),
|
||||||
|
}
|
||||||
|
|
||||||
if key.modifiers != KeyModifiers::NONE {
|
if key.modifiers != KeyModifiers::NONE {
|
||||||
|
last_input_valid = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match key.kind {
|
let input_event = match key.code {
|
||||||
KeyEventKind::Release => continue,
|
KeyCode::Enter => {
|
||||||
KeyEventKind::Press | KeyEventKind::Repeat => (),
|
if last_input_valid {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match key.code {
|
InputEvent::Unrecognized
|
||||||
KeyCode::Enter => {
|
}
|
||||||
let input_event = match input.trim() {
|
KeyCode::Char(c) => {
|
||||||
"n" | "next" => InputEvent::Next,
|
let input_event = match c {
|
||||||
"h" | "hint" => InputEvent::Hint,
|
'n' => InputEvent::Next,
|
||||||
"l" | "list" => break InputEvent::List,
|
'h' => InputEvent::Hint,
|
||||||
"q" | "quit" => break InputEvent::Quit,
|
'l' => break InputEvent::List,
|
||||||
"r" | "run" if manual_run => InputEvent::Run,
|
'q' => break InputEvent::Quit,
|
||||||
_ => InputEvent::Unrecognized(input.clone()),
|
'r' if manual_run => InputEvent::Run,
|
||||||
|
_ => {
|
||||||
|
last_input_valid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
last_input_valid = true;
|
||||||
|
input_event
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
last_input_valid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.clear();
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
input.push(c);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::Resize(_, _) => {
|
Event::Resize(_, _) => {
|
||||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
if tx.send(WatchEvent::TerminalResize).is_err() {
|
||||||
|
|
Loading…
Reference in a new issue