Merge branch 'main' into project

This commit is contained in:
mo8it 2024-03-27 15:06:58 +01:00
commit b24f256f2a
6 changed files with 323 additions and 177 deletions

82
Cargo.lock generated
View file

@ -201,6 +201,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "0.3.6" version = "0.3.6"
@ -213,6 +219,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.23" version = "0.2.23"
@ -360,6 +376,12 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.21" version = "0.4.21"
@ -527,6 +549,19 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rustix"
version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustlings" name = "rustlings"
version = "5.6.1" version = "5.6.1"
@ -536,14 +571,15 @@ dependencies = [
"clap", "clap",
"console", "console",
"glob", "glob",
"home",
"indicatif", "indicatif",
"notify-debouncer-mini", "notify-debouncer-mini",
"predicates", "predicates",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"toml", "shlex",
"toml_edit",
"which",
"winnow",
] ]
[[package]] [[package]]
@ -601,6 +637,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.0" version = "0.11.0"
@ -624,18 +666,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "toml"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.5" version = "0.6.5"
@ -647,9 +677,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.6" version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -701,6 +731,18 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "which"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
dependencies = [
"either",
"home",
"rustix",
"winsafe",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -872,3 +914,9 @@ checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"

View file

@ -12,13 +12,14 @@ edition = "2021"
anyhow = "1.0.81" anyhow = "1.0.81"
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.2", features = ["derive"] }
console = "0.15.8" console = "0.15.8"
home = "0.5.9"
indicatif = "0.17.8" indicatif = "0.17.8"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
regex = "1.10.3"
serde_json = "1.0.114" serde_json = "1.0.114"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
toml = "0.8.10" shlex = "1.3.0"
toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] }
which = "6.0.1"
winnow = "0.6.5"
[[bin]] [[bin]]
name = "rustlings" name = "rustlings"

View file

@ -1,19 +1,33 @@
use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use std::env;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::fs::{self, remove_file, File}; use std::fs::{self, remove_file, File};
use std::io::Read; use std::io::{self, BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{self, Command}; use std::process::{self, exit, Command, Stdio};
use std::{array, env, mem};
use winnow::ascii::{space0, Caseless};
use winnow::combinator::opt;
use winnow::Parser;
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"];
const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"];
const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
const CONTEXT: usize = 2; const CONTEXT: usize = 2;
const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml"; const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml";
// Checks 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()
}
// Get a temporary file name that is hopefully unique // Get a temporary file name that is hopefully unique
#[inline] #[inline]
fn temp_file() -> String { fn temp_file() -> String {
@ -58,7 +72,7 @@ pub struct Exercise {
// An enum to track of the state of an Exercise. // An enum to track of the state of an Exercise.
// An Exercise can be either Done or Pending // An Exercise can be either Done or Pending
#[derive(PartialEq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub enum State { pub enum State {
// The state of the exercise once it's been completed // The state of the exercise once it's been completed
Done, Done,
@ -67,7 +81,7 @@ pub enum State {
} }
// The context information of a pending exercise // The context information of a pending exercise
#[derive(PartialEq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub struct ContextLine { pub struct ContextLine {
// The source code that is still pending completion // The source code that is still pending completion
pub line: String, pub line: String,
@ -148,7 +162,10 @@ path = "{}.rs""#,
.args(RUSTC_COLOR_ARGS) .args(RUSTC_COLOR_ARGS)
.args(RUSTC_EDITION_ARGS) .args(RUSTC_EDITION_ARGS)
.args(RUSTC_NO_DEBUG_ARGS) .args(RUSTC_NO_DEBUG_ARGS)
.output() .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("Failed to compile!"); .expect("Failed to compile!");
// Due to an issue with Clippy, a cargo clean is required to catch all lints. // Due to an issue with Clippy, a cargo clean is required to catch all lints.
// See https://github.com/rust-lang/rust-clippy/issues/2604 // See https://github.com/rust-lang/rust-clippy/issues/2604
@ -157,7 +174,10 @@ path = "{}.rs""#,
Command::new("cargo") Command::new("cargo")
.args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH])
.args(RUSTC_COLOR_ARGS) .args(RUSTC_COLOR_ARGS)
.output() .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("Failed to run 'cargo clean'"); .expect("Failed to run 'cargo clean'");
Command::new("cargo") Command::new("cargo")
.args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH])
@ -205,51 +225,101 @@ path = "{}.rs""#,
} }
pub fn state(&self) -> State { pub fn state(&self) -> State {
let mut source_file = File::open(&self.path).unwrap_or_else(|e| { let source_file = File::open(&self.path).unwrap_or_else(|e| {
panic!( println!(
"We were unable to open the exercise file {}! {e}", "Failed to open the exercise file {}: {e}",
self.path.display() self.path.display(),
) );
exit(1);
}); });
let mut source_reader = BufReader::new(source_file);
let source = { // Read the next line into `buf` without the newline at the end.
let mut s = String::new(); let mut read_line = |buf: &mut String| -> io::Result<_> {
source_file.read_to_string(&mut s).unwrap_or_else(|e| { let n = source_reader.read_line(buf)?;
panic!( if buf.ends_with('\n') {
"We were unable to read the exercise file {}! {e}", buf.pop();
self.path.display() if buf.ends_with('\r') {
) buf.pop();
}); }
s }
Ok(n)
}; };
let re = Regex::new(I_AM_DONE_REGEX).unwrap(); 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);
if !re.is_match(&source) { 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 State::Done; return State::Done;
} }
let matched_line_index = source if contains_not_done_comment(&line) {
.lines() 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() .enumerate()
.find_map(|(i, line)| if re.is_match(line) { Some(i) } else { None }) .rev()
.expect("This should not happen at all"); {
context.push(ContextLine {
line: prev_line,
number: current_line_number - 1 - ind,
important: false,
});
}
let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize; // Current line.
let max_line = matched_line_index + CONTEXT; context.push(ContextLine {
line,
number: current_line_number,
important: true,
});
let context = source // Next lines.
.lines() for ind in 0..CONTEXT {
.enumerate() let mut next_line = String::with_capacity(256);
.filter(|&(i, _)| i >= min_line && i <= max_line) let Ok(n) = read_line(&mut next_line) else {
.map(|(i, line)| ContextLine { // If an error occurs, just ignore the next lines.
line: line.to_string(), break;
number: i + 1, };
important: i == matched_line_index,
})
.collect();
State::Pending(context) // 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 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();
}
} }
// Check that the exercise looks to be solved using self.state() // Check that the exercise looks to be solved using self.state()
@ -375,4 +445,20 @@ mod test {
let out = exercise.compile().unwrap().run().unwrap(); let out = exercise.compile().unwrap().run().unwrap();
assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); assert!(out.stdout.contains("THIS TEST TOO SHALL PASS"));
} }
#[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"));
}
} }

View file

@ -7,11 +7,12 @@ use clap::{Parser, Subcommand};
use console::Emoji; use console::Emoji;
use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::notify::{self, RecursiveMode};
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
use shlex::Shlex;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
use std::io::{self, prelude::*}; use std::io::{self, prelude::*};
use std::path::Path; use std::path::Path;
use std::process::{Command, Stdio}; use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::mpsc::{channel, RecvTimeoutError};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -92,24 +93,25 @@ fn main() -> Result<()> {
println!("\n{WELCOME}\n"); println!("\n{WELCOME}\n");
} }
if !Path::new("info.toml").exists() { if which::which("rustc").is_err() {
println!(
"{} must be run from the rustlings directory",
std::env::current_exe().unwrap().to_str().unwrap()
);
println!("Try `cd rustlings/`!");
std::process::exit(1);
}
if !rustc_exists() {
println!("We cannot find `rustc`."); println!("We cannot find `rustc`.");
println!("Try running `rustc --version` to diagnose your problem."); println!("Try running `rustc --version` to diagnose your problem.");
println!("For instructions on how to install Rust, check the README."); println!("For instructions on how to install Rust, check the README.");
std::process::exit(1); std::process::exit(1);
} }
let toml_str = &fs::read_to_string("info.toml").unwrap(); let info_file = fs::read_to_string("info.toml").unwrap_or_else(|e| {
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises; match e.kind() {
io::ErrorKind::NotFound => println!(
"The program must be run from the rustlings directory\nTry `cd rustlings/`!",
),
_ => println!("Failed to read the info.toml file: {e}"),
}
std::process::exit(1);
});
let exercises = toml_edit::de::from_str::<ExerciseList>(&info_file)
.unwrap()
.exercises;
let verbose = args.nocapture; let verbose = args.nocapture;
let command = args.command.unwrap_or_else(|| { let command = args.command.unwrap_or_else(|| {
@ -129,31 +131,43 @@ fn main() -> Result<()> {
println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
} }
let mut exercises_done: u16 = 0; let mut exercises_done: u16 = 0;
let filters = filter.clone().unwrap_or_default().to_lowercase(); let lowercase_filter = filter
exercises.iter().for_each(|e| { .as_ref()
let fname = format!("{}", e.path.display()); .map(|s| s.to_lowercase())
let filter_cond = filters .unwrap_or_default();
let filters = lowercase_filter
.split(',') .split(',')
.filter(|f| !f.trim().is_empty()) .filter_map(|f| {
.any(|f| e.name.contains(f) || fname.contains(f)); let f = f.trim();
let status = if e.looks_done() { if f.is_empty() {
None
} else {
Some(f)
}
})
.collect::<Vec<_>>();
for exercise in &exercises {
let fname = exercise.path.to_string_lossy();
let filter_cond = filters
.iter()
.any(|f| exercise.name.contains(f) || fname.contains(f));
let looks_done = exercise.looks_done();
let status = if looks_done {
exercises_done += 1; exercises_done += 1;
"Done" "Done"
} else { } else {
"Pending" "Pending"
}; };
let solve_cond = { let solve_cond =
(e.looks_done() && solved) (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved);
|| (!e.looks_done() && unsolved)
|| (!solved && !unsolved)
};
if solve_cond && (filter_cond || filter.is_none()) { if solve_cond && (filter_cond || filter.is_none()) {
let line = if paths { let line = if paths {
format!("{fname}\n") format!("{fname}\n")
} else if names { } else if names {
format!("{}\n", e.name) format!("{}\n", exercise.name)
} else { } else {
format!("{:<17}\t{fname:<46}\t{status:<7}\n", e.name) format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name)
}; };
// Somehow using println! leads to the binary panicking // Somehow using println! leads to the binary panicking
// when its output is piped. // when its output is piped.
@ -169,7 +183,8 @@ fn main() -> Result<()> {
}); });
} }
} }
}); }
let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0; let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0;
println!( println!(
"Progress: You completed {} / {} exercises ({:.1} %).", "Progress: You completed {} / {} exercises ({:.1} %).",
@ -208,16 +223,13 @@ fn main() -> Result<()> {
println!("Failed to write rust-project.json to disk for rust-analyzer: {e}"); println!("Failed to write rust-project.json to disk for rust-analyzer: {e}");
} else { } else {
println!("Successfully generated rust-project.json"); println!("Successfully generated rust-project.json");
println!("rust-analyzer will now parse exercises, restart your language server or editor") println!("rust-analyzer will now parse exercises, restart your language server or editor");
} }
} }
Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) {
Err(e) => { Err(e) => {
println!( println!("Error: Could not watch your progress. Error message was {e:?}.");
"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."); println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
std::process::exit(1); std::process::exit(1);
} }
@ -239,15 +251,23 @@ fn main() -> Result<()> {
} }
fn spawn_watch_shell( fn spawn_watch_shell(
failed_exercise_hint: &Arc<Mutex<Option<String>>>, failed_exercise_hint: Arc<Mutex<Option<String>>>,
should_quit: Arc<AtomicBool>, should_quit: Arc<AtomicBool>,
) { ) {
let failed_exercise_hint = Arc::clone(failed_exercise_hint);
println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
thread::spawn(move || loop {
let mut input = String::new(); thread::spawn(move || {
match io::stdin().read_line(&mut input) { let mut input = String::with_capacity(32);
Ok(_) => { 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(); let input = input.trim();
if input == "hint" { if input == "hint" {
if let Some(hint) = &*failed_exercise_hint.lock().unwrap() { if let Some(hint) = &*failed_exercise_hint.lock().unwrap() {
@ -255,37 +275,31 @@ fn spawn_watch_shell(
} }
} else if input == "clear" { } else if input == "clear" {
println!("\x1B[2J\x1B[1;1H"); println!("\x1B[2J\x1B[1;1H");
} else if input.eq("quit") { } else if input == "quit" {
should_quit.store(true, Ordering::SeqCst); should_quit.store(true, Ordering::SeqCst);
println!("Bye!"); println!("Bye!");
} else if input.eq("help") { } else if input == "help" {
println!("Commands available to you in watch mode:"); println!("{WATCH_MODE_HELP_MESSAGE}");
println!(" hint - prints the current exercise's hint");
println!(" clear - clears the screen");
println!(" quit - quits watch mode");
println!(" !<cmd> - executes a command, like `!rustc --explain E0381`");
println!(" help - displays this help message");
println!();
println!("Watch mode automatically re-evaluates the current exercise");
println!("when you edit a file's contents.")
} else if let Some(cmd) = input.strip_prefix('!') { } else if let Some(cmd) = input.strip_prefix('!') {
let parts: Vec<&str> = cmd.split_whitespace().collect(); let mut parts = Shlex::new(cmd);
if parts.is_empty() {
let Some(program) = parts.next() else {
println!("no command provided"); println!("no command provided");
} else if let Err(e) = Command::new(parts[0]).args(&parts[1..]).status() { continue;
println!("failed to execute command `{}`: {}", cmd, e); };
if let Err(e) = Command::new(program).args(parts).status() {
println!("failed to execute command `{cmd}`: {e}");
} }
} else { } else {
println!("unknown command: {input}"); println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
} }
} }
Err(error) => println!("error reading command: {error}"),
}
}); });
} }
fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise { fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise {
if name.eq("next") { if name == "next" {
exercises exercises
.iter() .iter()
.find(|e| !e.looks_done()) .find(|e| !e.looks_done())
@ -331,7 +345,6 @@ fn watch(
clear_screen(); clear_screen();
let to_owned_hint = |t: &Exercise| t.hint.to_owned();
let failed_exercise_hint = match verify( let failed_exercise_hint = match verify(
exercises.iter(), exercises.iter(),
(0, exercises.len()), (0, exercises.len()),
@ -339,9 +352,9 @@ fn watch(
success_hints, success_hints,
) { ) {
Ok(_) => return Ok(WatchStatus::Finished), Ok(_) => return Ok(WatchStatus::Finished),
Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))), Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))),
}; };
spawn_watch_shell(&failed_exercise_hint, Arc::clone(&should_quit)); spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit));
loop { loop {
match rx.recv_timeout(Duration::from_secs(1)) { match rx.recv_timeout(Duration::from_secs(1)) {
Ok(event) => match event { Ok(event) => match event {
@ -376,7 +389,7 @@ fn watch(
Err(exercise) => { Err(exercise) => {
let mut failed_exercise_hint = let mut failed_exercise_hint =
failed_exercise_hint.lock().unwrap(); failed_exercise_hint.lock().unwrap();
*failed_exercise_hint = Some(to_owned_hint(exercise)); *failed_exercise_hint = Some(exercise.hint.clone());
} }
} }
} }
@ -396,19 +409,7 @@ fn watch(
} }
} }
fn rustc_exists() -> bool { const DEFAULT_OUT: &str = "Thanks for installing Rustlings!
Command::new("rustc")
.args(["--version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.and_then(|mut child| child.wait())
.map(|status| status.success())
.unwrap_or(false)
}
const DEFAULT_OUT: &str = r#"Thanks for installing Rustlings!
Is this your first time? Don't worry, Rustlings was made for beginners! We are 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 going to teach you a lot of things about Rust, but before we can get
@ -434,7 +435,7 @@ started, here's a couple of notes about how Rustlings operates:
autocompletion, run the command `rustlings lsp`. autocompletion, run the command `rustlings lsp`.
Got all that? Great! To get started, run `rustlings watch` in order to get the first Got all that? Great! To get started, run `rustlings watch` in order to get the first
exercise. Make sure to have your editor open!"#; exercise. Make sure to have your editor open!";
const FENISH_LINE: &str = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |
@ -470,3 +471,13 @@ const WELCOME: &str = r" welcome to...
| | | |_| \__ \ |_| | | | | | (_| \__ \ | | | |_| \__ \ |_| | | | | | (_| \__ \
|_| \__,_|___/\__|_|_|_| |_|\__, |___/ |_| \__,_|___/\__|_|_|_| |_|\__, |___/
|___/"; |___/";
const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode:
hint - prints the current exercise's hint
clear - clears the screen
quit - quits watch mode
!<cmd> - executes a command, like `!rustc --explain E0381`
help - displays this help message
Watch mode automatically re-evaluates the current exercise
when you edit a file's contents.";

View file

@ -21,7 +21,8 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
// Resets the exercise by stashing the changes. // Resets the exercise by stashing the changes.
pub fn reset(exercise: &Exercise) -> Result<(), ()> { pub fn reset(exercise: &Exercise) -> Result<(), ()> {
let command = Command::new("git") let command = Command::new("git")
.args(["stash", "--"]) .arg("stash")
.arg("--")
.arg(&exercise.path) .arg(&exercise.path)
.spawn(); .spawn();

View file

@ -24,7 +24,7 @@ pub fn verify<'a>(
.progress_chars("#>-"), .progress_chars("#>-"),
); );
bar.set_position(num_done as u64); bar.set_position(num_done as u64);
bar.set_message(format!("({:.1} %)", percentage)); bar.set_message(format!("({percentage:.1} %)"));
for exercise in exercises { for exercise in exercises {
let compile_result = match exercise.mode { let compile_result = match exercise.mode {
@ -37,7 +37,7 @@ pub fn verify<'a>(
} }
percentage += 100.0 / total as f32; percentage += 100.0 / total as f32;
bar.inc(1); bar.inc(1);
bar.set_message(format!("({:.1} %)", percentage)); bar.set_message(format!("({percentage:.1} %)"));
if bar.position() == total as u64 { if bar.position() == total as u64 {
println!( println!(
"Progress: You completed {} / {} exercises ({:.1} %).", "Progress: You completed {} / {} exercises ({:.1} %).",
@ -51,6 +51,7 @@ pub fn verify<'a>(
Ok(()) Ok(())
} }
#[derive(PartialEq, Eq)]
enum RunMode { enum RunMode {
Interactive, Interactive,
NonInteractive, NonInteractive,
@ -124,7 +125,7 @@ fn compile_and_test(
if verbose { if verbose {
println!("{}", output.stdout); println!("{}", output.stdout);
} }
if let RunMode::Interactive = run_mode { if run_mode == RunMode::Interactive {
Ok(prompt_for_completion(exercise, None, success_hints)) Ok(prompt_for_completion(exercise, None, success_hints))
} else { } else {
Ok(true) Ok(true)
@ -191,27 +192,25 @@ fn prompt_for_completion(
Mode::Test => "The code is compiling, and the tests pass!", Mode::Test => "The code is compiling, and the tests pass!",
Mode::Clippy => clippy_success_msg, Mode::Clippy => clippy_success_msg,
}; };
println!();
if no_emoji { if no_emoji {
println!("~*~ {success_msg} ~*~") println!("\n~*~ {success_msg} ~*~\n");
} else { } else {
println!("🎉 🎉 {success_msg} 🎉 🎉") println!("\n🎉 🎉 {success_msg} 🎉 🎉\n");
} }
println!();
if let Some(output) = prompt_output { if let Some(output) = prompt_output {
println!("Output:"); println!(
println!("{}", separator()); "Output:\n{separator}\n{output}\n{separator}\n",
println!("{output}"); separator = separator(),
println!("{}", separator()); );
println!();
} }
if success_hints { if success_hints {
println!("Hints:"); println!(
println!("{}", separator()); "Hints:\n{separator}\n{}\n{separator}\n",
println!("{}", exercise.hint); exercise.hint,
println!("{}", separator()); separator = separator(),
println!(); );
} }
println!("You can keep working on this exercise,"); println!("You can keep working on this exercise,");
@ -224,14 +223,14 @@ fn prompt_for_completion(
let formatted_line = if context_line.important { let formatted_line = if context_line.important {
format!("{}", style(context_line.line).bold()) format!("{}", style(context_line.line).bold())
} else { } else {
context_line.line.to_string() context_line.line
}; };
println!( println!(
"{:>2} {} {}", "{:>2} {} {}",
style(context_line.number).blue().bold(), style(context_line.number).blue().bold(),
style("|").blue(), style("|").blue(),
formatted_line formatted_line,
); );
} }