Skip to content

Commit 247bd19

Browse files
committed
Canonicalize exercise paths only once
1 parent e5ed115 commit 247bd19

File tree

6 files changed

+77
-29
lines changed

6 files changed

+77
-29
lines changed

src/app_state.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
env,
44
fs::{File, OpenOptions},
55
io::{self, Read, Seek, StdoutLock, Write},
6-
path::Path,
6+
path::{Path, MAIN_SEPARATOR_STR},
77
process::{Command, Stdio},
88
thread,
99
};
@@ -15,6 +15,7 @@ use crate::{
1515
embedded::EMBEDDED_FILES,
1616
exercise::{Exercise, RunnableExercise},
1717
info_file::ExerciseInfo,
18+
term,
1819
};
1920

2021
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
@@ -71,6 +72,7 @@ impl AppState {
7172
format!("Failed to open or create the state file {STATE_FILE_NAME}")
7273
})?;
7374

75+
let dir_canonical_path = term::canonicalize("exercises");
7476
let mut exercises = exercise_infos
7577
.into_iter()
7678
.map(|exercise_info| {
@@ -82,10 +84,32 @@ impl AppState {
8284
let dir = exercise_info.dir.map(|dir| &*dir.leak());
8385
let hint = exercise_info.hint.leak().trim_ascii();
8486

87+
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
88+
let mut canonical_path;
89+
if let Some(dir) = dir {
90+
canonical_path = String::with_capacity(
91+
2 + dir_canonical_path.len() + dir.len() + name.len(),
92+
);
93+
canonical_path.push_str(dir_canonical_path);
94+
canonical_path.push_str(MAIN_SEPARATOR_STR);
95+
canonical_path.push_str(dir);
96+
} else {
97+
canonical_path =
98+
String::with_capacity(1 + dir_canonical_path.len() + name.len());
99+
canonical_path.push_str(dir_canonical_path);
100+
}
101+
102+
canonical_path.push_str(MAIN_SEPARATOR_STR);
103+
canonical_path.push_str(name);
104+
canonical_path.push_str(".rs");
105+
canonical_path
106+
});
107+
85108
Exercise {
86109
dir,
87110
name,
88111
path,
112+
canonical_path,
89113
test: exercise_info.test,
90114
strict_clippy: exercise_info.strict_clippy,
91115
hint,
@@ -486,6 +510,7 @@ mod tests {
486510
dir: None,
487511
name: "0",
488512
path: "exercises/0.rs",
513+
canonical_path: None,
489514
test: false,
490515
strict_clippy: false,
491516
hint: "",

src/exercise.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::io::{self, StdoutLock, Write};
77

88
use crate::{
99
cmd::CmdRunner,
10-
term::{terminal_file_link, write_ansi},
10+
term::{self, terminal_file_link, write_ansi, CountedWrite},
1111
};
1212

1313
/// The initial capacity of the output buffer.
@@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R
1818
stdout.write_all(b"Solution")?;
1919
stdout.queue(ResetColor)?;
2020
stdout.write_all(b" for comparison: ")?;
21-
terminal_file_link(stdout, solution_path, Color::Cyan)?;
21+
if let Some(canonical_path) = term::canonicalize(solution_path) {
22+
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
23+
} else {
24+
stdout.write_all(solution_path.as_bytes())?;
25+
}
2226
stdout.write_all(b"\n")
2327
}
2428

@@ -60,12 +64,23 @@ pub struct Exercise {
6064
pub name: &'static str,
6165
/// Path of the exercise file starting with the `exercises/` directory.
6266
pub path: &'static str,
67+
pub canonical_path: Option<String>,
6368
pub test: bool,
6469
pub strict_clippy: bool,
6570
pub hint: &'static str,
6671
pub done: bool,
6772
}
6873

74+
impl Exercise {
75+
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
76+
if let Some(canonical_path) = self.canonical_path.as_deref() {
77+
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
78+
}
79+
80+
writer.write_str(self.path)
81+
}
82+
}
83+
6984
pub trait RunnableExercise {
7085
fn name(&self) -> &str;
7186
fn dir(&self) -> Option<&str>;

src/list/state.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::{
1313
use crate::{
1414
app_state::AppState,
1515
exercise::Exercise,
16-
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
16+
term::{progress_bar, CountedWrite, MaxLenWriter},
1717
};
1818

1919
use super::scroll_state::ScrollState;
@@ -158,7 +158,7 @@ impl<'a> ListState<'a> {
158158
if self.app_state.vs_code() {
159159
writer.write_str(exercise.path)?;
160160
} else {
161-
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
161+
exercise.terminal_file_link(&mut writer)?;
162162
}
163163

164164
next_ln(stdout)?;

src/run.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use std::{
1111
use crate::{
1212
app_state::{AppState, ExercisesProgress},
1313
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
14-
term::terminal_file_link,
1514
};
1615

1716
pub fn run(app_state: &mut AppState) -> Result<()> {
@@ -26,7 +25,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
2625
app_state.set_pending(app_state.current_exercise_ind())?;
2726

2827
stdout.write_all(b"Ran ")?;
29-
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
28+
app_state
29+
.current_exercise()
30+
.terminal_file_link(&mut stdout)?;
3031
stdout.write_all(b" with errors\n")?;
3132
exit(1);
3233
}
@@ -46,7 +47,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
4647
match app_state.done_current_exercise(&mut stdout)? {
4748
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
4849
stdout.write_all(b"Next exercise: ")?;
49-
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
50+
app_state
51+
.current_exercise()
52+
.terminal_file_link(&mut stdout)?;
5053
stdout.write_all(b"\n")?;
5154
}
5255
ExercisesProgress::AllDone => (),

src/term.rs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
use std::{
2-
fmt, fs,
3-
io::{self, BufRead, StdoutLock, Write},
4-
};
5-
61
use crossterm::{
72
cursor::MoveTo,
83
style::{Attribute, Color, SetAttribute, SetForegroundColor},
94
terminal::{Clear, ClearType},
105
Command, QueueableCommand,
116
};
7+
use std::{
8+
fmt, fs,
9+
io::{self, BufRead, StdoutLock, Write},
10+
};
1211

1312
pub struct MaxLenWriter<'a, 'b> {
1413
pub stdout: &'a mut StdoutLock<'b>,
@@ -151,25 +150,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
151150
stdout.write_all(b"\n")
152151
}
153152

153+
/// Canonicalize, convert to string and remove verbatim part on Windows.
154+
pub fn canonicalize(path: &str) -> Option<String> {
155+
fs::canonicalize(path)
156+
.ok()?
157+
.into_os_string()
158+
.into_string()
159+
.ok()
160+
.map(|mut path| {
161+
// Windows itself can't handle its verbatim paths.
162+
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
163+
path.drain(..4);
164+
}
165+
166+
path
167+
})
168+
}
169+
154170
pub fn terminal_file_link<'a>(
155171
writer: &mut impl CountedWrite<'a>,
156172
path: &str,
173+
canonical_path: &str,
157174
color: Color,
158175
) -> io::Result<()> {
159-
let canonical_path = fs::canonicalize(path).ok();
160-
161-
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
162-
return writer.write_str(path);
163-
};
164-
165-
// Windows itself can't handle its verbatim paths.
166-
#[cfg(windows)]
167-
let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" {
168-
&canonical_path[4..]
169-
} else {
170-
canonical_path
171-
};
172-
173176
writer
174177
.stdout()
175178
.queue(SetForegroundColor(color))?

src/watch/state.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
app_state::{AppState, ExercisesProgress},
1212
clear_terminal,
1313
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
14-
term::{progress_bar, terminal_file_link},
14+
term::progress_bar,
1515
};
1616

1717
#[derive(PartialEq, Eq)]
@@ -184,7 +184,9 @@ impl<'a> WatchState<'a> {
184184
)?;
185185

186186
stdout.write_all(b"\nCurrent exercise: ")?;
187-
terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?;
187+
self.app_state
188+
.current_exercise()
189+
.terminal_file_link(stdout)?;
188190
stdout.write_all(b"\n\n")?;
189191

190192
self.show_prompt(stdout)?;

0 commit comments

Comments
 (0)