diff --git a/src/app_state.rs b/src/app_state.rs index 381aaf8a..7123d11a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -3,7 +3,7 @@ use std::{ env, fs::{File, OpenOptions}, io::{self, Read, Seek, StdoutLock, Write}, - path::Path, + path::{Path, MAIN_SEPARATOR_STR}, process::{Command, Stdio}, thread, }; @@ -15,6 +15,7 @@ use crate::{ embedded::EMBEDDED_FILES, exercise::{Exercise, RunnableExercise}, info_file::ExerciseInfo, + term, }; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; @@ -71,6 +72,7 @@ impl AppState { format!("Failed to open or create the state file {STATE_FILE_NAME}") })?; + let dir_canonical_path = term::canonicalize("exercises"); let mut exercises = exercise_infos .into_iter() .map(|exercise_info| { @@ -82,10 +84,32 @@ impl AppState { let dir = exercise_info.dir.map(|dir| &*dir.leak()); let hint = exercise_info.hint.leak().trim_ascii(); + let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { + let mut canonical_path; + if let Some(dir) = dir { + canonical_path = String::with_capacity( + 2 + dir_canonical_path.len() + dir.len() + name.len(), + ); + canonical_path.push_str(dir_canonical_path); + canonical_path.push_str(MAIN_SEPARATOR_STR); + canonical_path.push_str(dir); + } else { + canonical_path = + String::with_capacity(1 + dir_canonical_path.len() + name.len()); + canonical_path.push_str(dir_canonical_path); + } + + canonical_path.push_str(MAIN_SEPARATOR_STR); + canonical_path.push_str(name); + canonical_path.push_str(".rs"); + canonical_path + }); + Exercise { dir, name, path, + canonical_path, test: exercise_info.test, strict_clippy: exercise_info.strict_clippy, hint, @@ -486,6 +510,7 @@ mod tests { dir: None, name: "0", path: "exercises/0.rs", + canonical_path: None, test: false, strict_clippy: false, hint: "", diff --git a/src/exercise.rs b/src/exercise.rs index 11eea638..7fb2343c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -7,7 +7,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, - term::{terminal_file_link, write_ansi}, + term::{self, terminal_file_link, write_ansi, CountedWrite}, }; /// The initial capacity of the output buffer. @@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R stdout.write_all(b"Solution")?; stdout.queue(ResetColor)?; stdout.write_all(b" for comparison: ")?; - terminal_file_link(stdout, solution_path, Color::Cyan)?; + 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())?; + } stdout.write_all(b"\n") } @@ -60,12 +64,23 @@ pub struct Exercise { pub name: &'static str, /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, + pub canonical_path: Option, pub test: bool, pub strict_clippy: bool, pub hint: &'static str, pub done: bool, } +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 trait RunnableExercise { fn name(&self) -> &str; fn dir(&self) -> Option<&str>; diff --git a/src/list/state.rs b/src/list/state.rs index 468049ab..ed7c71f6 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -13,7 +13,7 @@ use std::{ use crate::{ app_state::AppState, exercise::Exercise, - term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter}, + term::{progress_bar, CountedWrite, MaxLenWriter}, }; use super::scroll_state::ScrollState; @@ -158,7 +158,7 @@ impl<'a> ListState<'a> { if self.app_state.vs_code() { writer.write_str(exercise.path)?; } else { - terminal_file_link(&mut writer, exercise.path, Color::Blue)?; + exercise.terminal_file_link(&mut writer)?; } next_ln(stdout)?; diff --git a/src/run.rs b/src/run.rs index 929b4751..f0faa69c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,6 @@ use std::{ use crate::{ app_state::{AppState, ExercisesProgress}, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, - term::terminal_file_link, }; pub fn run(app_state: &mut AppState) -> Result<()> { @@ -26,7 +25,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> { app_state.set_pending(app_state.current_exercise_ind())?; stdout.write_all(b"Ran ")?; - terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + app_state + .current_exercise() + .terminal_file_link(&mut stdout)?; stdout.write_all(b" with errors\n")?; exit(1); } @@ -46,7 +47,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => { stdout.write_all(b"Next exercise: ")?; - terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?; + app_state + .current_exercise() + .terminal_file_link(&mut stdout)?; stdout.write_all(b"\n")?; } ExercisesProgress::AllDone => (), diff --git a/src/term.rs b/src/term.rs index 489d6585..5b557ecf 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,14 +1,13 @@ -use std::{ - fmt, fs, - io::{self, BufRead, StdoutLock, Write}, -}; - use crossterm::{ cursor::MoveTo, style::{Attribute, Color, SetAttribute, SetForegroundColor}, terminal::{Clear, ClearType}, Command, QueueableCommand, }; +use std::{ + fmt, fs, + io::{self, BufRead, StdoutLock, Write}, +}; pub struct MaxLenWriter<'a, 'b> { pub stdout: &'a mut StdoutLock<'b>, @@ -151,25 +150,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { stdout.write_all(b"\n") } +/// Canonicalize, convert to string and remove verbatim part on Windows. +pub fn canonicalize(path: &str) -> Option { + fs::canonicalize(path) + .ok()? + .into_os_string() + .into_string() + .ok() + .map(|mut path| { + // Windows itself can't handle its verbatim paths. + if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") { + path.drain(..4); + } + + path + }) +} + pub fn terminal_file_link<'a>( writer: &mut impl CountedWrite<'a>, path: &str, + canonical_path: &str, color: Color, ) -> io::Result<()> { - let canonical_path = fs::canonicalize(path).ok(); - - let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else { - return writer.write_str(path); - }; - - // Windows itself can't handle its verbatim paths. - #[cfg(windows)] - let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" { - &canonical_path[4..] - } else { - canonical_path - }; - writer .stdout() .queue(SetForegroundColor(color))? diff --git a/src/watch/state.rs b/src/watch/state.rs index 1c2e2a9a..fe9e2748 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -11,7 +11,7 @@ use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY}, - term::{progress_bar, terminal_file_link}, + term::progress_bar, }; #[derive(PartialEq, Eq)] @@ -184,7 +184,9 @@ impl<'a> WatchState<'a> { )?; stdout.write_all(b"\nCurrent exercise: ")?; - terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?; + self.app_state + .current_exercise() + .terminal_file_link(stdout)?; stdout.write_all(b"\n\n")?; self.show_prompt(stdout)?;