Compare commits

...

37 commits

Author SHA1 Message Date
mo8it b24f256f2a Merge branch 'main' into project 2024-03-27 15:06:58 +01:00
Mo 864d046058
Merge pull request #1912 from mo8it/performance
Optimizations 1
2024-03-27 15:02:25 +01:00
mo8it a27741b131 Merge branch 'main' into performance 2024-03-27 15:00:57 +01:00
Mo b13bafa13e
Merge pull request #1911 from mo8it/watch
Improvements to watch mode
2024-03-27 14:42:17 +01:00
mo8it f995b4c041 Merge branch 'main' into watch 2024-03-27 14:41:26 +01:00
Mo b8a5886db4
Merge pull request #1914 from mo8it/toml
Reading the `info.toml` file
2024-03-27 14:30:59 +01:00
mo8it b9d2756ce8 Merge branch 'main' into toml 2024-03-27 14:30:10 +01:00
Mo 07dec76f7c
Merge pull request #1916 from mo8it/command
Pipe the output of command to null instead of capturing and ignoring it
2024-03-27 14:24:16 +01:00
Mo deeefcf16c
Merge pull request #1913 from mo8it/which
Use `which` instead of running `rustc --version`
2024-03-27 14:21:11 +01:00
mo8it 8e0f7e56f7 Merge branch 'main' into which 2024-03-27 14:18:20 +01:00
Mo 87ca05b4bb
Merge pull request #1915 from mo8it/home
Remove the home dependency since it is not used
2024-03-27 12:58:30 +01:00
Mo d69a8a7045
Merge pull request #1921 from mo8it/style
Style
2024-03-27 12:57:26 +01:00
mo8it 87001a68c0 The string doesn't have to be a raw string 2024-03-26 17:50:29 +01:00
mo8it a610fc1bc2 Remove unneeded closure 2024-03-26 17:50:10 +01:00
mo8it e89028581c Use == instead of eq 2024-03-26 17:49:55 +01:00
mo8it 980ffa2a2b Use == on simple enums 2024-03-26 17:49:48 +01:00
mo8it 1f2029ae55 Add missing semicolon 2024-03-26 17:49:25 +01:00
mo8it ed0fcf8e3d Formatting 2024-03-26 17:49:05 +01:00
mo8it f36efae25d Only use arg instead of args AND arg 2024-03-26 17:48:06 +01:00
mo8it 853d0593d0 Derive Eq when PartialEq is derived 2024-03-26 17:47:33 +01:00
mo8it 078f6ffc1c Add comments 2024-03-26 02:26:26 +01:00
mo8it 7a6f71f090 Fix context of previous lines and improve readability 2024-03-26 02:14:25 +01:00
mo8it d911586788 Pipe the output to null instead of capturing and ignoring it 2024-03-25 17:21:54 +01:00
mo8it dca3ea355e Remove the home dependency since it is not used 2024-03-25 14:10:51 +01:00
mo8it e4520602f5 Use the NotFound variant of the IO error 2024-03-25 02:41:45 +01:00
mo8it 83cd91ccca Replace toml with toml_edit 2024-03-25 02:35:51 +01:00
mo8it 51b4c240ed Use which instead of running rustc --version 2024-03-25 00:30:01 +01:00
mo8it bdf826a026 Make "I AM NOT DONE" caseless 2024-03-24 22:22:55 +01:00
mo8it c0c112985b Replace regex with winnow 2024-03-24 19:18:19 +01:00
mo8it f205ee3d4c Call looks_done only once 2024-03-24 18:50:46 +01:00
mo8it e1375ef431 Use to_string_lossy 2024-03-24 18:47:27 +01:00
mo8it 0aeaccc3a5 Optimize state 2024-03-24 18:34:46 +01:00
mo8it 01b7d6334c Remove unneeded to_string call 2024-03-23 22:08:25 +01:00
mo8it a325df55d1 Cache filters 2024-03-23 21:56:40 +01:00
mo8it 27fa7c3e4a Move the const string to the bottom like others 2024-03-23 19:00:15 +01:00
mo8it 0d93266462 Initialize the input buffer with some capacity 2024-03-23 18:56:30 +01:00
mo8it 3dce7e5696 Improvements to watch mode 2024-03-23 18:51:25 +01:00
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 {
return State::Done; 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;
}
if contains_not_done_comment(&line) {
let mut context = Vec::with_capacity(2 * CONTEXT + 1);
// Previous lines.
for (ind, prev_line) in prev_lines
.into_iter()
.take(current_line_number - 1)
.enumerate()
.rev()
{
context.push(ContextLine {
line: prev_line,
number: current_line_number - 1 - ind,
important: false,
});
}
// Current line.
context.push(ContextLine {
line,
number: current_line_number,
important: true,
});
// Next lines.
for ind in 0..CONTEXT {
let mut next_line = String::with_capacity(256);
let Ok(n) = read_line(&mut next_line) else {
// If an error occurs, just ignore the next lines.
break;
};
// Reached the end of the file.
if n == 0 {
break;
}
context.push(ContextLine {
line: next_line,
number: current_line_number + 1 + ind,
important: false,
});
}
return 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();
} }
let matched_line_index = source
.lines()
.enumerate()
.find_map(|(i, line)| if re.is_match(line) { Some(i) } else { None })
.expect("This should not happen at all");
let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize;
let max_line = matched_line_index + CONTEXT;
let context = source
.lines()
.enumerate()
.filter(|&(i, _)| i >= min_line && i <= max_line)
.map(|(i, line)| ContextLine {
line: line.to_string(),
number: i + 1,
important: i == matched_line_index,
})
.collect();
State::Pending(context)
} }
// 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())
.unwrap_or_default();
let filters = lowercase_filter
.split(',')
.filter_map(|f| {
let f = f.trim();
if f.is_empty() {
None
} else {
Some(f)
}
})
.collect::<Vec<_>>();
for exercise in &exercises {
let fname = exercise.path.to_string_lossy();
let filter_cond = filters let filter_cond = filters
.split(',') .iter()
.filter(|f| !f.trim().is_empty()) .any(|f| exercise.name.contains(f) || fname.contains(f));
.any(|f| e.name.contains(f) || fname.contains(f)); let looks_done = exercise.looks_done();
let status = if e.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,53 +251,55 @@ 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();
let input = input.trim();
if input == "hint" { loop {
if let Some(hint) = &*failed_exercise_hint.lock().unwrap() { // Recycle input buffer.
println!("{hint}"); input.clear();
}
} else if input == "clear" { if let Err(e) = stdin.read_line(&mut input) {
println!("\x1B[2J\x1B[1;1H"); println!("error reading command: {e}");
} else if input.eq("quit") { }
should_quit.store(true, Ordering::SeqCst);
println!("Bye!"); let input = input.trim();
} else if input.eq("help") { if input == "hint" {
println!("Commands available to you in watch mode:"); if let Some(hint) = &*failed_exercise_hint.lock().unwrap() {
println!(" hint - prints the current exercise's hint"); println!("{hint}");
println!(" clear - clears the screen"); }
println!(" quit - quits watch mode"); } else if input == "clear" {
println!(" !<cmd> - executes a command, like `!rustc --explain E0381`"); println!("\x1B[2J\x1B[1;1H");
println!(" help - displays this help message"); } else if input == "quit" {
println!(); should_quit.store(true, Ordering::SeqCst);
println!("Watch mode automatically re-evaluates the current exercise"); println!("Bye!");
println!("when you edit a file's contents.") } else if input == "help" {
} else if let Some(cmd) = input.strip_prefix('!') { println!("{WATCH_MODE_HELP_MESSAGE}");
let parts: Vec<&str> = cmd.split_whitespace().collect(); } else if let Some(cmd) = input.strip_prefix('!') {
if parts.is_empty() { let mut parts = Shlex::new(cmd);
println!("no command provided");
} else if let Err(e) = Command::new(parts[0]).args(&parts[1..]).status() { let Some(program) = parts.next() else {
println!("failed to execute command `{}`: {}", cmd, e); println!("no command provided");
} continue;
} else { };
println!("unknown command: {input}");
} if let Err(e) = Command::new(program).args(parts).status() {
println!("failed to execute command `{cmd}`: {e}");
}
} else {
println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}");
} }
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,
); );
} }