1
0
Fork 0
mirror of https://github.com/rust-lang/rustlings.git synced 2024-06-06 09:26:15 +02:00

Merge branch 'main' into performance

This commit is contained in:
mo8it 2024-03-27 15:00:57 +01:00
commit a27741b131
6 changed files with 169 additions and 116 deletions

80
Cargo.lock generated
View File

@ -195,6 +195,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"
@ -207,6 +213,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"
@ -354,6 +370,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"
@ -521,6 +543,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"
@ -529,13 +564,14 @@ dependencies = [
"clap", "clap",
"console", "console",
"glob", "glob",
"home",
"indicatif", "indicatif",
"notify-debouncer-mini", "notify-debouncer-mini",
"predicates", "predicates",
"serde", "serde",
"serde_json", "serde_json",
"toml", "shlex",
"toml_edit",
"which",
"winnow", "winnow",
] ]
@ -594,6 +630,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"
@ -617,18 +659,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"
@ -640,9 +670,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",
@ -694,6 +724,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"
@ -865,3 +907,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,12 +12,13 @@ edition = "2021"
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.2", features = ["derive"] }
console = "0.15.8" console = "0.15.8"
glob = "0.3.0" glob = "0.3.0"
home = "0.5.9"
indicatif = "0.17.8" indicatif = "0.17.8"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
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" winnow = "0.6.5"
[[bin]] [[bin]]

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Display, Formatter};
use std::fs::{self, remove_file, File}; use std::fs::{self, remove_file, File};
use std::io::{self, BufRead, BufReader}; use std::io::{self, BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{self, exit, Command}; use std::process::{self, exit, Command, Stdio};
use std::{array, env, mem}; use std::{array, env, mem};
use winnow::ascii::{space0, Caseless}; use winnow::ascii::{space0, Caseless};
use winnow::combinator::opt; use winnow::combinator::opt;
@ -72,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,
@ -81,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,
@ -162,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
@ -171,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])

View File

@ -6,11 +6,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};
@ -91,24 +92,25 @@ fn main() {
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(|| {
@ -230,16 +232,13 @@ fn main() {
println!("Failed to write rust-project.json to disk for rust-analyzer"); println!("Failed to write rust-project.json to disk for rust-analyzer");
} 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);
} }
@ -259,53 +258,55 @@ fn main() {
} }
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())
@ -351,7 +352,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()),
@ -359,9 +359,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 {
@ -396,7 +396,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());
} }
} }
} }
@ -416,19 +416,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
@ -454,7 +442,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! |
@ -490,3 +478,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,");
@ -231,7 +230,7 @@ fn prompt_for_completion(
"{:>2} {} {}", "{:>2} {} {}",
style(context_line.number).blue().bold(), style(context_line.number).blue().bold(),
style("|").blue(), style("|").blue(),
formatted_line formatted_line,
); );
} }