Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exclude = [
]

[workspace.package]
version = "6.4.0"
version = "6.5.0"
authors = [
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
Expand All @@ -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"] }
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions exercises/13_error_handling/errors4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`.
// Read the tests below to clarify what should be returned.
Ok(Self(value as u64))
}
}
Expand Down
2 changes: 2 additions & 0 deletions exercises/18_iterators/iterators3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions exercises/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
2 changes: 1 addition & 1 deletion release-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions solutions/18_iterators/iterators3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 6 additions & 6 deletions src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]| {
Expand Down
8 changes: 4 additions & 4 deletions src/embedded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
60 changes: 36 additions & 24 deletions src/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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)
Expand All @@ -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)
}
})
}
}

Expand Down
25 changes: 9 additions & 16 deletions src/list/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,21 @@ 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))
.and_then(|(pre_highlight, rest)| {
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)
Expand Down Expand Up @@ -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()..])?;

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,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);
Expand Down
Loading
Loading