diff --git a/CHANGELOG.md b/CHANGELOG.md index c1dbb42bbe..18e0aa660c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,23 @@ ## Unreleased +## 6.5.0 (2025-08-21) + +### Added + +- Check that Clippy is installed before initialization + ### Changed - Upgrade to Rust edition 2024 -- Raise the minimum supported Rust version to `1.87` +- Raise the minimum supported Rust version to `1.88` +- Don't follow symlinks in the file watcher +- `dev new`: Don't add `.rustlings-state.txt` to `.gitignore` + +### Fixed + +- Fix file links in VS Code +- Fix error printing when the progress bar is shown +- `dev check`: Don't check formatting if there are no solution files ## 6.4.0 (2024-11-11) diff --git a/Cargo.lock b/Cargo.lock index a0c2f08051..f883653fd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.4.0" +version = "6.5.0" dependencies = [ "anyhow", "clap", @@ -455,7 +455,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.4.0" +version = "6.5.0" dependencies = [ "quote", "serde", @@ -505,9 +505,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -579,15 +579,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -684,11 +684,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 56adbb5cb9..4469b28112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ exclude = [ ] [workspace.package] -version = "6.4.0" +version = "6.5.0" authors = [ "Mo Bitar ", # https://github.com/mo8it "Liv ", # https://github.com/shadows-withal @@ -15,7 +15,7 @@ authors = [ repository = "https://github.com/rust-lang/rustlings" license = "MIT" edition = "2024" # On Update: Update the edition of `rustfmt` in `dev check` and `CARGO_TOML` in `dev new`. -rust-version = "1.87" +rust-version = "1.88" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } @@ -49,7 +49,7 @@ anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } crossterm = { version = "0.29", default-features = false, features = ["windows", "events"] } notify = "8.0" -rustlings-macros = { path = "rustlings-macros", version = "=6.4.0" } +rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" } serde_json = "1.0" serde.workspace = true toml.workspace = true @@ -58,7 +58,7 @@ toml.workspace = true rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] } [dev-dependencies] -tempfile = "3.19" +tempfile = "3.21" [profile.release] panic = "abort" diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index ba01e54bf5..144fce7b22 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -10,6 +10,7 @@ struct PositiveNonzeroInteger(u64); impl PositiveNonzeroInteger { fn new(value: i64) -> Result { // TODO: This function shouldn't always return an `Ok`. + // Read the tests below to clarify what should be returned. Ok(Self(value as u64)) } } diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 6b1eca1734..dce09055dd 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -39,6 +39,8 @@ mod tests { #[test] fn test_success() { assert_eq!(divide(81, 9), Ok(9)); + assert_eq!(divide(81, -1), Ok(-81)); + assert_eq!(divide(i64::MIN, i64::MIN), Ok(1)); } #[test] diff --git a/exercises/README.md b/exercises/README.md index 237f2f1edc..1df5cc3757 100644 --- a/exercises/README.md +++ b/exercises/README.md @@ -22,6 +22,6 @@ | iterators | §13.2-4 | | smart_pointers | §15, §16.3 | | threads | §16.1-3 | -| macros | §19.5 | -| clippy | §21.4 | +| macros | §20.5 | +| clippy | Appendix D | | conversions | n/a | diff --git a/release-hook.sh b/release-hook.sh index 42135369ab..4934933793 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -13,4 +13,4 @@ cargo test --workspace cargo dev check --require-solutions # MSRV -cargo +1.87 dev check --require-solutions +cargo +1.88 dev check --require-solutions diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs index 11aa1ec8ce..1d5d67f29f 100644 --- a/solutions/18_iterators/iterators3.rs +++ b/solutions/18_iterators/iterators3.rs @@ -52,6 +52,8 @@ mod tests { #[test] fn test_success() { assert_eq!(divide(81, 9), Ok(9)); + assert_eq!(divide(81, -1), Ok(-81)); + assert_eq!(divide(i64::MIN, i64::MIN), Ok(1)); } #[test] diff --git a/src/app_state.rs b/src/app_state.rs index f3f348133e..d654d0425d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -60,8 +60,7 @@ pub struct AppState { file_buf: Vec, 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; 3]| { diff --git a/src/embedded.rs b/src/embedded.rs index 88c1fb0139..61a5f581e2 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -20,10 +20,10 @@ struct ExerciseFiles { } fn create_dir_if_not_exists(path: &str) -> Result<()> { - if let Err(e) = create_dir(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(Error::from(e).context(format!("Failed to create the directory {path}"))); - } + if let Err(e) = create_dir(path) + && e.kind() != io::ErrorKind::AlreadyExists + { + return Err(Error::from(e).context(format!("Failed to create the directory {path}"))); } Ok(()) diff --git a/src/exercise.rs b/src/exercise.rs index fdfbc4f6ea..a0596b5b9e 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") } @@ -42,17 +48,17 @@ fn run_bin( let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; - if let Some(output) = output { - if !success { - // This output is important to show the user that something went wrong. - // Otherwise, calling something like `exit(1)` in an exercise without further output - // leaves the user confused about why the exercise isn't done yet. - write_ansi(output, SetAttribute(Attribute::Bold)); - write_ansi(output, SetForegroundColor(Color::Red)); - output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); - write_ansi(output, ResetColor); - output.push(b'\n'); - } + if let Some(output) = output + && !success + { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. + write_ansi(output, SetAttribute(Attribute::Bold)); + write_ansi(output, SetForegroundColor(Color::Red)); + output.extend_from_slice(b"The exercise didn't run successfully (nonzero exit code)"); + write_ansi(output, ResetColor); + output.push(b'\n'); } Ok(success) @@ -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/list/state.rs b/src/list/state.rs index ae65ec2be9..4fd1301d6b 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -118,8 +118,8 @@ impl<'a> ListState<'a> { } fn draw_exercise_name(&self, writer: &mut MaxLenWriter, exercise: &Exercise) -> io::Result<()> { - if !self.search_query.is_empty() { - if let Some((pre_highlight, highlight, post_highlight)) = exercise + if !self.search_query.is_empty() + && let Some((pre_highlight, highlight, post_highlight)) = exercise .name .find(&self.search_query) .and_then(|ind| exercise.name.split_at_checked(ind)) @@ -127,13 +127,12 @@ impl<'a> ListState<'a> { rest.split_at_checked(self.search_query.len()) .map(|x| (pre_highlight, x.0, x.1)) }) - { - writer.write_str(pre_highlight)?; - writer.stdout.queue(SetForegroundColor(Color::Magenta))?; - writer.write_str(highlight)?; - writer.stdout.queue(SetForegroundColor(Color::Reset))?; - return writer.write_str(post_highlight); - } + { + writer.write_str(pre_highlight)?; + writer.stdout.queue(SetForegroundColor(Color::Magenta))?; + writer.write_str(highlight)?; + writer.stdout.queue(SetForegroundColor(Color::Reset))?; + return writer.write_str(post_highlight); } writer.write_str(exercise.name) @@ -186,13 +185,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 29de56b3d2..ffd2dfa75b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,7 +167,7 @@ fn main() -> Result { } 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 6f4f099b47..b473fc2062 100644 --- a/src/run.rs +++ b/src/run.rs @@ -27,7 +27,7 @@ pub fn run(app_state: &mut AppState) -> Result { 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 { 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 { 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 b7dcd9f101..3d149b33e8 100644 --- a/src/term.rs +++ b/src/term.rs @@ -272,22 +272,18 @@ pub fn canonicalize(path: &str) -> Option { }) } -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))? @@ -296,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, command: impl Command) { struct FmtWriter<'a>(&'a mut Vec); diff --git a/src/watch/state.rs b/src/watch/state.rs index 2413becd28..a92dd2d6d7 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)?;