Compare commits

...

20 commits

Author SHA1 Message Date
mo8it 4ce8667b9d Show the exercise name in the waiting message 2024-08-08 22:48:53 +02:00
mo8it 0785b24192 Show a message before running the exercise 2024-08-08 22:41:41 +02:00
mo8it 34f02cf83d Attach error message as context 2024-08-08 22:37:56 +02:00
mo8it 8df66f7991 Allow initialization in a workspace 2024-08-08 02:45:18 +02:00
mo8it 39580381fa rust-analyzer problem isn't fixed :( 2024-08-08 01:48:57 +02:00
mo8it 06a0f278e5 Don't recommend the builtin VS-Code terminal because it can't clear scrollback 2024-08-08 01:35:47 +02:00
mo8it fd97470f35 Adapt type name in hint 2024-08-08 00:42:26 +02:00
mo8it 11fc3f1e56 Fix errors not being shown after the welcome message 2024-08-08 00:41:12 +02:00
mo8it 693bb708b2 Add README to the solutions dir 2024-08-08 00:41:12 +02:00
mo8it 97719fe8da Remove state file and solutions dir from .gitignore 2024-08-08 00:41:12 +02:00
mo8it 4933ace50b Add panic = "abort" for exercises 2024-08-08 00:41:12 +02:00
mo8it 81bf0a6430 Remove redundant rustfmt check for solutions 2024-08-08 00:41:12 +02:00
mo8it 24aed1b14e Update CHANGELOG 2024-08-08 00:41:12 +02:00
Mo 09c3ac02f8
Merge pull request #2062 from jimbo5922/jimbo5922-fix-hashmap3-struct-name
update struct name in hashmap3
2024-08-08 00:40:51 +02:00
Mo 45a39585b3
Merge pull request #2066 from matthewjnield/main
chore: Fix snakecase convention in errors6.rs
2024-08-08 00:36:46 +02:00
mo8it 286a455fa9 Avoid using RUSTFLAGS to not trigger rebuilding, especially in rust-analyzer 2024-08-07 23:35:50 +02:00
mo8it bdf4960b6a Fix exercise name shift in exercise check 2024-08-07 23:25:22 +02:00
Matt Nield 2128be8b28
chore: Fix snakecase convention in errors6.rs
Exercise errors6.rs prompts the user to add a method named `from_parseint`. This commit changes the method name to the corrected snakecase format, `from_parse_int`.
2024-08-04 02:36:45 -04:00
Yudai Kawabuchi e65ae09789 fix format 2024-08-01 09:55:25 +09:00
Yudai Kawabuchi dacdce1ea2 fix: update struct name in hashmap3 2024-08-01 09:47:50 +09:00
19 changed files with 146 additions and 110 deletions

View file

@ -1,8 +1,10 @@
<a name="6.1.1"></a> <a name="6.2.0"></a>
## 6.1.1 (UNRELEASED) ## 6.2.0 (2024-08-08)
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
- Remove the state file and the solutions directory from the generated `.gitignore` file.
- Add a `README.md` file to the `solutions/` directory.
- Run the final check of all exercises in parallel. - Run the final check of all exercises in parallel.
- Small exercise improvements. - Small exercise improvements.
- `dev check`: Check that all solutions are formatted with `rustfmt`. - `dev check`: Check that all solutions are formatted with `rustfmt`.

View file

@ -88,8 +88,6 @@ While working with Rustlings, please use a modern terminal for the best user exp
The default terminal on Linux and Mac should be sufficient. The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
If you use VS Code, the builtin terminal should also be fine.
## Doing exercises ## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`. The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.

View file

@ -195,3 +195,9 @@ name = "exercises"
edition = "2021" edition = "2021"
# Don't publish the exercises on crates.io! # Don't publish the exercises on crates.io!
publish = false publish = false
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team. // A structure to store the goal details of a team.
#[derive(Default)] #[derive(Default)]
struct Team { struct TeamScores {
goals_scored: u8, goals_scored: u8,
goals_conceded: u8, goals_conceded: u8,
} }
fn build_scores_table(results: &str) -> HashMap<&str, Team> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::new();

View file

@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
} }
// TODO: Add another error conversion function here. // TODO: Add another error conversion function here.
// fn from_parseint(???) -> Self { ??? } // fn from_parse_int(???) -> Self { ??? }
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]

View file

@ -9,6 +9,5 @@ cargo upgrades
# Similar to CI # Similar to CI
cargo clippy -- --deny warnings cargo clippy -- --deny warnings
cargo fmt --all --check cargo fmt --all --check
rustfmt --check --edition 2021 solutions/**/*.rs
cargo test --workspace --all-targets cargo test --workspace --all-targets
cargo run -- dev check --require-solutions cargo run -- dev check --require-solutions

View file

@ -571,7 +571,7 @@ name = "hashmaps3"
dir = "11_hashmaps" dir = "11_hashmaps"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
`HashMap` to insert the default value of `Team` if a team doesn't `HashMap` to insert the default value of `TeamScores` if a team doesn't
exist in the table yet. exist in the table yet.
Learn more in The Book: Learn more in The Book:

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team. // A structure to store the goal details of a team.
#[derive(Default)] #[derive(Default)]
struct Team { struct TeamScores {
goals_scored: u8, goals_scored: u8,
goals_conceded: u8, goals_conceded: u8,
} }
fn build_scores_table(results: &str) -> HashMap<&str, Team> { fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::new();
@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet. // Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_insert_with(Team::default); let team_1 = scores
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values. // Update the values.
team_1.goals_scored += team_1_score; team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score; team_1.goals_conceded += team_2_score;
// Similarely for the second team. // Similarely for the second team.
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default); let team_2 = scores
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score; team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score; team_2.goals_conceded += team_1_score;
} }

View file

@ -24,7 +24,7 @@ impl ParsePosNonzeroError {
Self::Creation(err) Self::Creation(err)
} }
fn from_parseint(err: ParseIntError) -> Self { fn from_parse_int(err: ParseIntError) -> Self {
Self::ParseInt(err) Self::ParseInt(err)
} }
} }
@ -44,7 +44,7 @@ impl PositiveNonzeroInteger {
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> { fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// Return an appropriate error instead of panicking when `parse()` // Return an appropriate error instead of panicking when `parse()`
// returns an error. // returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Self::new(x).map_err(ParsePosNonzeroError::from_creation) Self::new(x).map_err(ParsePosNonzeroError::from_creation)
} }

6
solutions/README.md Normal file
View file

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

View file

@ -129,13 +129,6 @@ impl<'out> CargoSubcommand<'out> {
self self
} }
/// RUSTFLAGS="-A warnings"
#[inline]
pub fn hide_warnings(&mut self) -> &mut Self {
self.cmd.env("RUSTFLAGS", "-A warnings");
self
}
/// 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.
#[inline] #[inline]
pub fn run(self, description: &str) -> Result<bool> { pub fn run(self, description: &str) -> Result<bool> {

View file

@ -175,22 +175,21 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res
return None; return None;
} }
Some(s.spawn(|| exercise_info.run_exercise(None, cmd_runner))) Some((
exercise_info.name.as_str(),
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else { let Ok(result) = handle.join() else {
bail!( bail!("Panic while trying to run the exericse {exercise_name}");
"Panic while trying to run the exericse {}",
exercise_info.name,
);
}; };
match result { match result {
Ok(true) => bail!( Ok(true) => bail!(
"The exercise {} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", "The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
exercise_info.name,
), ),
Ok(false) => (), Ok(false) => (),
Err(e) => return Err(e), Err(e) => return Err(e),

View file

@ -76,8 +76,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
pub const GITIGNORE: &[u8] = b".rustlings-state.txt pub const GITIGNORE: &[u8] = b".rustlings-state.txt
Cargo.lock Cargo.lock
target target/
.vscode .vscode/
!.vscode/extensions.json !.vscode/extensions.json
"; ";

View file

@ -78,27 +78,40 @@ pub trait RunnableExercise {
mut output: Option<&mut Vec<u8>>, mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner, cmd_runner: &CmdRunner,
) -> Result<bool> { ) -> Result<bool> {
let output_is_none = if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
output.clear(); output.clear();
false
} else {
true
};
let mut build_cmd = cmd_runner.cargo("build", bin_name, output.as_deref_mut());
if output_is_none {
build_cmd.hide_warnings();
} }
let build_success = build_cmd.run("cargo build …")?;
let build_success = cmd_runner
.cargo("build", bin_name, output.as_deref_mut())
.run("cargo build …")?;
if !build_success { if !build_success {
return Ok(false); return Ok(false);
} }
// Discard the output of `cargo build` because it will be shown again by Clippy. // Discard the compiler output because it will be shown again by `cargo test` or Clippy.
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
output.clear(); output.clear();
} }
if self.test() {
let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some {
test_cmd.args(["--", "--color", "always", "--show-output"]);
}
let test_success = test_cmd.run("cargo test …")?;
if !test_success {
run_bin(bin_name, output, cmd_runner)?;
return Ok(false);
}
// Discard the compiler output because it will be shown again by Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
}
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `[cfg(test)]`. // `--profile test` is required to also check code with `[cfg(test)]`.
@ -109,25 +122,9 @@ pub trait RunnableExercise {
} }
let clippy_success = clippy_cmd.run("cargo clippy …")?; let clippy_success = clippy_cmd.run("cargo clippy …")?;
if !clippy_success {
return Ok(false);
}
if !self.test() {
return run_bin(bin_name, output.as_deref_mut(), cmd_runner);
}
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if !output_is_none {
test_cmd.args(["--", "--color", "always", "--show-output"]);
}
// Hide warnings because they are shown by Clippy.
test_cmd.hide_warnings();
let test_success = test_cmd.run("cargo test …")?;
let run_success = run_bin(bin_name, output, cmd_runner)?; let run_success = run_bin(bin_name, output, cmd_runner)?;
Ok(test_success && run_success) Ok(clippy_success && run_success)
} }
/// Compile, check and run the exercise. /// Compile, check and run the exercise.

View file

@ -3,30 +3,40 @@ use ratatui::crossterm::style::Stylize;
use std::{ use std::{
env::set_current_dir, env::set_current_dir,
fs::{self, create_dir}, fs::{self, create_dir},
io::ErrorKind, io::{self, Write},
path::Path, path::Path,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
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,
term::press_enter_prompt,
};
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
// Prevent initialization in a directory that contains the file `Cargo.toml`. let rustlings_dir = Path::new("rustlings");
// This can mean that Rustlings was already initialized in this directory. if rustlings_dir.exists() {
// Otherwise, this can cause problems with Cargo workspaces. bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
let mut stdout = io::stdout().lock();
let mut init_git = true;
if Path::new("Cargo.toml").exists() { if Path::new("Cargo.toml").exists() {
bail!(CARGO_TOML_EXISTS_ERR); if Path::new("exercises").exists() && Path::new("solutions").exists() {
} bail!(IN_INITIALIZED_DIR_ERR);
let rustlings_path = Path::new("rustlings");
if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
} }
return Err(e.into());
stdout.write_all(CARGO_TOML_EXISTS_PROMPT_MSG)?;
press_enter_prompt(&mut stdout)?;
init_git = false;
} }
set_current_dir("rustlings") stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
press_enter_prompt(&mut stdout)?;
create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?;
set_current_dir(rustlings_dir)
.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()?;
@ -35,6 +45,11 @@ pub fn init() -> Result<()> {
.context("Failed to initialize the `rustlings/exercises` directory")?; .context("Failed to initialize the `rustlings/exercises` directory")?;
create_dir("solutions").context("Failed to create the `solutions/` directory")?; create_dir("solutions").context("Failed to create the `solutions/` directory")?;
fs::write(
"solutions/README.md",
include_bytes!("../solutions/README.md"),
)
.context("Failed to create the file rustlings/solutions/README.md")?;
for dir in EMBEDDED_FILES.exercise_dirs { for dir in EMBEDDED_FILES.exercise_dirs {
let mut dir_path = String::with_capacity(10 + dir.name.len()); let mut dir_path = String::with_capacity(10 + dir.name.len());
dir_path.push_str("solutions/"); dir_path.push_str("solutions/");
@ -70,18 +85,21 @@ pub fn init() -> Result<()> {
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?; .context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
// Ignore any Git error because Git initialization is not required. if init_git {
let _ = Command::new("git") // Ignore any Git error because Git initialization is not required.
.arg("init") let _ = Command::new("git")
.stdin(Stdio::null()) .arg("init")
.stderr(Stdio::null()) .stdin(Stdio::null())
.status(); .stderr(Stdio::null())
.status();
}
println!( writeln!(
stdout,
"\n{}\n\n{}", "\n{}\n\n{}",
"Initialization done ✓".green(), "Initialization done ✓".green(),
POST_INIT_MSG.bold(), POST_INIT_MSG.bold(),
); )?;
Ok(()) Ok(())
} }
@ -92,16 +110,14 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
} }
"; ";
const GITIGNORE: &[u8] = b".rustlings-state.txt const GITIGNORE: &[u8] = b"Cargo.lock
solutions target/
Cargo.lock .vscode/
target
.vscode
"; ";
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 CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`. const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. 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."; Otherwise, please run `rustlings init` again in another directory.";
@ -112,5 +128,19 @@ You probably already initialized Rustlings.
Run `cd rustlings` Run `cd rustlings`
Then run `rustlings` again"; Then run `rustlings` again";
const CARGO_TOML_EXISTS_PROMPT_MSG: &[u8] = br#"You are about to initialize Rustlings in a directory that already contains a `Cargo.toml` file!
=> It is recommended to abort with CTRL+C and initialize Rustlings in another directory <=
If you know what you are doing and want to initialize Rustlings in a Cargo workspace,
then you need to add its directory to `members` in the `workspace` section of the `Cargo.toml` file:
```toml
[workspace]
members = ["rustlings"]
```
Press ENTER if you are sure that you want to continue after reading the warning above "#;
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started."; Then run `rustlings` to get started.";

View file

@ -2,10 +2,11 @@ use anyhow::{bail, Context, Result};
use app_state::StateFileStatus; use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::{ use std::{
io::{self, BufRead, IsTerminal, StdoutLock, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
process::exit, process::exit,
}; };
use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
@ -20,20 +21,12 @@ mod init;
mod list; mod list;
mod progress_bar; mod progress_bar;
mod run; mod run;
mod term;
mod terminal_link; mod terminal_link;
mod watch; mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1; const CURRENT_FORMAT_VERSION: u8 = 1;
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
fn press_enter_prompt() -> io::Result<()> {
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
Ok(())
}
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)] #[derive(Parser)]
#[command(version)] #[command(version)]
@ -79,14 +72,6 @@ fn main() -> Result<()> {
match args.command { match args.command {
Some(Subcommands::Init) => { Some(Subcommands::Init) => {
{
let mut stdout = io::stdout().lock();
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
stdout.write_all(b"\n")?;
}
return init::init().context("Initialization failed"); return init::init().context("Initialization failed");
} }
Some(Subcommands::Dev(dev_command)) => return dev_command.run(), Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
@ -118,9 +103,10 @@ fn main() -> Result<()> {
let welcome_message = welcome_message.trim_ascii(); let welcome_message = welcome_message.trim_ascii();
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
stdout.flush()?; press_enter_prompt(&mut stdout)?;
press_enter_prompt()?;
clear_terminal(&mut stdout)?; clear_terminal(&mut stdout)?;
// Flush to be able to show errors occuring before printing a newline to stdout.
stdout.flush()?;
} }
StateFileStatus::Read => (), StateFileStatus::Read => (),
} }

12
src/term.rs Normal file
View file

@ -0,0 +1,12 @@
use std::io::{self, BufRead, StdoutLock, Write};
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?;
Ok(())
}

View file

@ -102,8 +102,7 @@ pub fn watch(
watch_state.render()?; watch_state.render()?;
} }
WatchEvent::NotifyErr(e) => { WatchEvent::NotifyErr(e) => {
watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?; return Err(Error::from(e).context(NOTIFY_ERR));
return Err(Error::from(e));
} }
WatchEvent::TerminalEventErr(e) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")); return Err(Error::from(e).context("Terminal event listener failed"));

View file

@ -51,6 +51,11 @@ impl<'a> WatchState<'a> {
pub fn run_current_exercise(&mut self) -> Result<()> { pub fn run_current_exercise(&mut self) -> Result<()> {
self.show_hint = false; self.show_hint = false;
writeln!(
self.writer,
"\nChecking the exercise `{}`. Please wait…",
self.app_state.current_exercise().name,
)?;
let success = self let success = self
.app_state .app_state
.current_exercise() .current_exercise()