diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_state.rs | 12 | ||||
| -rw-r--r-- | src/cargo_toml.rs | 9 | ||||
| -rw-r--r-- | src/dev/check.rs | 50 | ||||
| -rw-r--r-- | src/dev/new.rs | 3 | ||||
| -rw-r--r-- | src/embedded.rs | 2 | ||||
| -rw-r--r-- | src/exercise.rs | 38 | ||||
| -rw-r--r-- | src/info_file.rs | 4 | ||||
| -rw-r--r-- | src/init.rs | 38 | ||||
| -rw-r--r-- | src/list/state.rs | 8 | ||||
| -rw-r--r-- | src/main.rs | 8 | ||||
| -rw-r--r-- | src/run.rs | 6 | ||||
| -rw-r--r-- | src/term.rs | 60 | ||||
| -rw-r--r-- | src/watch/state.rs | 4 |
13 files changed, 158 insertions, 84 deletions
diff --git a/src/app_state.rs b/src/app_state.rs index f3f3481..d654d04 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -60,8 +60,7 @@ pub struct AppState { file_buf: Vec<u8>, official_exercises: bool, cmd_runner: CmdRunner, - // Running in VS Code. - vs_code: bool, + emit_file_links: bool, } impl AppState { @@ -181,7 +180,8 @@ impl AppState { file_buf, official_exercises: !Path::new("info.toml").exists(), cmd_runner, - vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), + // VS Code has its own file link handling + emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), }; Ok((slf, state_file_status)) @@ -218,8 +218,8 @@ impl AppState { } #[inline] - pub fn vs_code(&self) -> bool { - self.vs_code + pub fn emit_file_links(&self) -> bool { + self.emit_file_links } // Write the state file. @@ -621,7 +621,7 @@ mod tests { file_buf: Vec::new(), official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), - vs_code: false, + emit_file_links: true, }; let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| { diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index e966809..ce0dfd0 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -134,7 +134,14 @@ mod tests { ); assert_eq!( - updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(), + updated_cargo_toml( + &exercise_infos, + "abc\n\ + bin = [xxx]\n\ + 123", + b"../" + ) + .unwrap(), br#"abc bin = [ { name = "1", path = "../exercises/1.rs" }, diff --git a/src/dev/check.rs b/src/dev/check.rs index 9cde7f2..f711106 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -15,6 +15,7 @@ use crate::{ cmd::CmdRunner, exercise::{OUTPUT_CAPACITY, RunnableExercise}, info_file::{ExerciseInfo, InfoFile}, + term::ProgressCounter, }; const MAX_N_EXERCISES: usize = 999; @@ -105,13 +106,15 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> { if !file_buf.contains("fn main()") { bail!( - "The `main` function is missing in the file `{path}`.\nCreate at least an empty `main` function to avoid language server errors" + "The `main` function is missing in the file `{path}`.\n\ + Create at least an empty `main` function to avoid language server errors" ); } if !file_buf.contains("// TODO") { bail!( - "Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user." + "Didn't find any `// TODO` comment in the file `{path}`.\n\ + You need to have at least one such comment to guide the user." ); } @@ -217,10 +220,7 @@ fn check_exercises_unsolved( .collect::<Result<Vec<_>, _>>() .context("Failed to spawn a thread to check if an exercise is already solved")?; - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; + let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?; for (exercise_name, handle) in handles { let Ok(result) = handle.join() else { @@ -229,17 +229,17 @@ fn check_exercises_unsolved( match result { Ok(true) => { - bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",) + bail!( + "The exercise {exercise_name} is already solved.\n\ + {SKIP_CHECK_UNSOLVED_HINT}", + ) } Ok(false) => (), Err(e) => return Err(e), } - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; + progress_counter.increment()?; } - stdout.write_all(b"\n")?; Ok(()) } @@ -247,10 +247,12 @@ fn check_exercises_unsolved( fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { Ordering::Less => bail!( - "`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version" + "`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\n\ + Please migrate to the latest format version" ), Ordering::Greater => bail!( - "`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program" + "`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\n\ + Try updating the Rustlings program" ), Ordering::Equal => (), } @@ -318,10 +320,7 @@ fn check_solutions( .arg("always") .stdin(Stdio::null()); - let n_handles = handles.len(); - write!(stdout, "Progress: 0/{n_handles}")?; - stdout.flush()?; - let mut handle_num = 1; + let mut progress_counter = ProgressCounter::new(&mut stdout, handles.len())?; for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { let Ok(check_result) = handle.join() else { @@ -338,7 +337,7 @@ fn check_solutions( } SolutionCheck::MissingOptional => (), SolutionCheck::RunFailure { output } => { - stdout.write_all(b"\n\n")?; + drop(progress_counter); stdout.write_all(&output)?; bail!( "Running the solution of the exercise {} failed with the error above", @@ -348,22 +347,21 @@ fn check_solutions( SolutionCheck::Err(e) => return Err(e), } - write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; - stdout.flush()?; - handle_num += 1; + progress_counter.increment()?; } - stdout.write_all(b"\n")?; + let n_solutions = sol_paths.len(); let handle = thread::Builder::new() .spawn(move || check_unexpected_files("solutions", &sol_paths)) .context( "Failed to spawn a thread to check for unexpected files in the solutions directory", )?; - if !fmt_cmd - .status() - .context("Failed to run `rustfmt` on all solution files")? - .success() + if n_solutions > 0 + && !fmt_cmd + .status() + .context("Failed to run `rustfmt` on all solution files")? + .success() { bail!("Some solutions aren't formatted. Run `rustfmt` on them"); } diff --git a/src/dev/new.rs b/src/dev/new.rs index 883b6fa..7c72a6b 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -78,8 +78,7 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { Ok(()) } -pub const GITIGNORE: &[u8] = b".rustlings-state.txt -Cargo.lock +pub const GITIGNORE: &[u8] = b"Cargo.lock target/ .vscode/ !.vscode/extensions.json diff --git a/src/embedded.rs b/src/embedded.rs index 51a14b6..88c1fb0 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -152,7 +152,7 @@ mod tests { #[test] fn dirs() { - let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file) + let exercises = toml::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/exercise.rs b/src/exercise.rs index fdfbc4f..6f517be 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -7,22 +7,28 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, - term::{self, CountedWrite, terminal_file_link, write_ansi}, + term::{self, CountedWrite, file_path, terminal_file_link, write_ansi}, }; /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; -pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> { +pub fn solution_link_line( + stdout: &mut StdoutLock, + solution_path: &str, + emit_file_links: bool, +) -> io::Result<()> { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"Solution")?; stdout.queue(ResetColor)?; stdout.write_all(b" for comparison: ")?; - if let Some(canonical_path) = term::canonicalize(solution_path) { - terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?; - } else { - stdout.write_all(solution_path.as_bytes())?; - } + file_path(stdout, Color::Cyan, |writer| { + if emit_file_links && let Some(canonical_path) = term::canonicalize(solution_path) { + terminal_file_link(writer, solution_path, &canonical_path) + } else { + writer.stdout().write_all(solution_path.as_bytes()) + } + })?; stdout.write_all(b"\n") } @@ -72,12 +78,18 @@ pub struct Exercise { } impl Exercise { - pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> { - if let Some(canonical_path) = self.canonical_path.as_deref() { - return terminal_file_link(writer, self.path, canonical_path, Color::Blue); - } - - writer.write_str(self.path) + pub fn terminal_file_link<'a>( + &self, + writer: &mut impl CountedWrite<'a>, + emit_file_links: bool, + ) -> io::Result<()> { + file_path(writer, Color::Blue, |writer| { + if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { + terminal_file_link(writer, self.path, canonical_path) + } else { + writer.write_str(self.path) + } + }) } } diff --git a/src/info_file.rs b/src/info_file.rs index 634bece..04e5d64 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -95,11 +95,11 @@ impl InfoFile { pub fn parse() -> Result<Self> { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { - Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content) + Ok(file_content) => toml::de::from_str::<Self>(&file_content) .context("Failed to parse the `info.toml` file")?, Err(e) => { if e.kind() == ErrorKind::NotFound { - return toml_edit::de::from_str(EMBEDDED_FILES.info_file) + return toml::de::from_str(EMBEDDED_FILES.info_file) .context("Failed to parse the embedded `info.toml` file"); } diff --git a/src/init.rs b/src/init.rs index a60fba7..68011ed 100644 --- a/src/init.rs +++ b/src/init.rs @@ -35,7 +35,27 @@ pub fn init() -> Result<()> { .stdin(Stdio::null()) .stderr(Stdio::null()) .output() - .context(CARGO_LOCATE_PROJECT_ERR)?; + .context( + "Failed to run the command `cargo locate-project …`\n\ + Did you already install Rust?\n\ + Try running `cargo --version` to diagnose the problem.", + )?; + + if !Command::new("cargo") + .arg("clippy") + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .context("Failed to run the command `cargo clippy --version`")? + .success() + { + bail!( + "Clippy, the official Rust linter, is missing.\n\ + Please install it first before initializing Rustlings." + ) + } let mut stdout = io::stdout().lock(); let mut init_git = true; @@ -58,11 +78,13 @@ pub fn init() -> Result<()> { && !workspace_manifest_content.contains("workspace.") { bail!( - "The current directory is already part of a Cargo project.\nPlease initialize Rustlings in a different directory" + "The current directory is already part of a Cargo project.\n\ + Please initialize Rustlings in a different directory" ); } - stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\nPress ENTER to continue ")?; + stdout.write_all(b"This command will create the directory `rustlings/` as a member of this Cargo workspace.\n\ + Press ENTER to continue ")?; press_enter_prompt(&mut stdout)?; // Make sure "rustlings" is added to `workspace.members` by making @@ -78,7 +100,8 @@ pub fn init() -> Result<()> { .status()?; if !status.success() { bail!( - "Failed to initialize a new Cargo workspace member.\nPlease initialize Rustlings in a different directory" + "Failed to initialize a new Cargo workspace member.\n\ + Please initialize Rustlings in a different directory" ); } @@ -87,7 +110,8 @@ pub fn init() -> Result<()> { .context("Failed to remove the temporary directory `rustlings/`")?; init_git = false; } else { - stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\n\ + Press ENTER to continue ")?; press_enter_prompt(&mut stdout)?; } @@ -166,10 +190,6 @@ pub fn init() -> Result<()> { Ok(()) } -const CARGO_LOCATE_PROJECT_ERR: &str = "Failed to run the command `cargo locate-project …` -Did you already install Rust? -Try running `cargo --version` to diagnose the problem."; - const INIT_SOLUTION_FILE: &[u8] = b"fn main() { // DON'T EDIT THIS SOLUTION FILE! // It will be automatically filled after you finish the exercise. diff --git a/src/list/state.rs b/src/list/state.rs index ae65ec2..50d06be 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -186,13 +186,7 @@ impl<'a> ListState<'a> { writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; - // The list links aren't shown correctly in VS Code on Windows. - // But VS Code shows its own links anyway. - if self.app_state.vs_code() { - writer.write_str(exercise.path)?; - } else { - exercise.terminal_file_link(&mut writer)?; - } + exercise.terminal_file_link(&mut writer, self.app_state.emit_file_links())?; writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?; diff --git a/src/main.rs b/src/main.rs index bce2593..ffd2dfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,11 @@ fn main() -> Result<ExitCode> { clear_terminal(&mut stdout)?; let welcome_message = welcome_message.trim_ascii(); - write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + write!( + stdout, + "{welcome_message}\n\n\ + Press ENTER to continue " + )?; press_enter_prompt(&mut stdout)?; clear_terminal(&mut stdout)?; // Flush to be able to show errors occurring before printing a newline to stdout. @@ -163,7 +167,7 @@ fn main() -> Result<ExitCode> { } app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n")?; return Ok(ExitCode::FAILURE); @@ -27,7 +27,7 @@ pub fn run(app_state: &mut AppState) -> Result<ExitCode> { stdout.write_all(b"Ran ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b" with errors\n")?; return Ok(ExitCode::FAILURE); @@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<ExitCode> { if let Some(solution_path) = app_state.current_solution_path()? { stdout.write_all(b"\n")?; - solution_link_line(&mut stdout, &solution_path)?; + solution_link_line(&mut stdout, &solution_path, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } @@ -50,7 +50,7 @@ pub fn run(app_state: &mut AppState) -> Result<ExitCode> { stdout.write_all(b"Next exercise: ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } ExercisesProgress::AllDone => (), diff --git a/src/term.rs b/src/term.rs index 1e08c84..3d149b3 100644 --- a/src/term.rs +++ b/src/term.rs @@ -160,6 +160,37 @@ impl<'a, 'lock> CheckProgressVisualizer<'a, 'lock> { } } +pub struct ProgressCounter<'a, 'lock> { + stdout: &'a mut StdoutLock<'lock>, + total: usize, + counter: usize, +} + +impl<'a, 'lock> ProgressCounter<'a, 'lock> { + pub fn new(stdout: &'a mut StdoutLock<'lock>, total: usize) -> io::Result<Self> { + write!(stdout, "Progress: 0/{total}")?; + stdout.flush()?; + + Ok(Self { + stdout, + total, + counter: 0, + }) + } + + pub fn increment(&mut self) -> io::Result<()> { + self.counter += 1; + write!(self.stdout, "\rProgress: {}/{}", self.counter, self.total)?; + self.stdout.flush() + } +} + +impl Drop for ProgressCounter<'_, '_> { + fn drop(&mut self) { + let _ = self.stdout.write_all(b"\n\n"); + } +} + pub fn progress_bar<'a>( writer: &mut impl CountedWrite<'a>, progress: u16, @@ -241,22 +272,18 @@ pub fn canonicalize(path: &str) -> Option<String> { }) } -pub fn terminal_file_link<'a>( - writer: &mut impl CountedWrite<'a>, - path: &str, - canonical_path: &str, +pub fn file_path<'a, W: CountedWrite<'a>>( + writer: &mut W, color: Color, + f: impl FnOnce(&mut W) -> io::Result<()>, ) -> io::Result<()> { writer .stdout() .queue(SetForegroundColor(color))? .queue(SetAttribute(Attribute::Underlined))?; - writer.stdout().write_all(b"\x1b]8;;file://")?; - writer.stdout().write_all(canonical_path.as_bytes())?; - writer.stdout().write_all(b"\x1b\\")?; - // Only this part is visible. - writer.write_str(path)?; - writer.stdout().write_all(b"\x1b]8;;\x1b\\")?; + + f(writer)?; + writer .stdout() .queue(SetForegroundColor(Color::Reset))? @@ -265,6 +292,19 @@ pub fn terminal_file_link<'a>( Ok(()) } +pub fn terminal_file_link<'a>( + writer: &mut impl CountedWrite<'a>, + path: &str, + canonical_path: &str, +) -> io::Result<()> { + writer.stdout().write_all(b"\x1b]8;;file://")?; + writer.stdout().write_all(canonical_path.as_bytes())?; + writer.stdout().write_all(b"\x1b\\")?; + // Only this part is visible. + writer.write_str(path)?; + writer.stdout().write_all(b"\x1b]8;;\x1b\\") +} + pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) { struct FmtWriter<'a>(&'a mut Vec<u8>); diff --git a/src/watch/state.rs b/src/watch/state.rs index 2413bec..a92dd2d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -233,7 +233,7 @@ impl<'a> WatchState<'a> { stdout.write_all(b"\n")?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - solution_link_line(stdout, solution_path)?; + solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?; } stdout.write_all( @@ -252,7 +252,7 @@ impl<'a> WatchState<'a> { stdout.write_all(b"\nCurrent exercise: ")?; self.app_state .current_exercise() - .terminal_file_link(stdout)?; + .terminal_file_link(stdout, self.app_state.emit_file_links())?; stdout.write_all(b"\n\n")?; self.show_prompt(stdout)?; |
