summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app_state.rs12
-rw-r--r--src/cargo_toml.rs9
-rw-r--r--src/dev/check.rs50
-rw-r--r--src/dev/new.rs3
-rw-r--r--src/embedded.rs2
-rw-r--r--src/exercise.rs38
-rw-r--r--src/info_file.rs4
-rw-r--r--src/init.rs38
-rw-r--r--src/list/state.rs8
-rw-r--r--src/main.rs8
-rw-r--r--src/run.rs6
-rw-r--r--src/term.rs60
-rw-r--r--src/watch/state.rs4
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);
diff --git a/src/run.rs b/src/run.rs
index 6f4f099..b473fc2 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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)?;