Skip to content
Merged
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@

## Unreleased

### Added

- Automatically open the current file if Rustlings is running in a VS Code terminal
- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev)
- New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij
- New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise
- Show the file link of the current exercise when running `rustlings hint` and `rustlings reset`

### Fixed

- Fix integer overflow on big terminal widths [@gabfec](https://github.com/gabfec)
- Fix workspace detection on Windows [@senekor](https://github.com/senekor)

### Changed

- Avoid initializing a nested Git repository [@senekor](https://github.com/senekor)
- `vecs2`: Removed the use of `map` and `collect`, which are only taught later.

## 6.5.0 (2025-08-21)
Expand Down
51 changes: 29 additions & 22 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ notify = "8"
rustlings-macros = { path = "rustlings-macros", version = "=6.5.0" }
serde_json = "1"
serde.workspace = true
shlex = "1"
toml.workspace = true

[target.'cfg(not(windows))'.dependencies]
Expand Down
52 changes: 36 additions & 16 deletions src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use anyhow::{Context, Error, Result, bail};
use crossterm::{QueueableCommand, cursor, terminal};
use std::{
collections::HashSet,
env,
fs::{File, OpenOptions},
io::{Read, Seek, StdoutLock, Write},
path::{MAIN_SEPARATOR_STR, Path},
Expand All @@ -17,6 +16,7 @@ use std::{
use crate::{
clear_terminal,
cmd::CmdRunner,
editor::{Editor, EditorJoinHandle},
embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo,
Expand Down Expand Up @@ -61,12 +61,15 @@ pub struct AppState {
official_exercises: bool,
cmd_runner: CmdRunner,
emit_file_links: bool,
editor: Option<Editor>,
}

impl AppState {
pub fn new(
exercise_infos: Vec<ExerciseInfo>,
final_message: &'static str,
editor: Option<Editor>,
vs_code_term: bool,
) -> Result<(Self, StateFileStatus)> {
let cmd_runner = CmdRunner::build()?;
let mut state_file = OpenOptions::new()
Expand Down Expand Up @@ -108,7 +111,9 @@ impl AppState {
Exercise {
name: exercise_info.name,
dir: exercise_info.dir,
path: exercise_info.path(),
// Leaking for `Editor::open`.
// Leaking is fine since the app state exists until the end of the program.
path: exercise_info.path().leak(),
canonical_path,
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
Expand Down Expand Up @@ -174,43 +179,37 @@ impl AppState {
official_exercises: !Path::new("info.toml").exists(),
cmd_runner,
// VS Code has its own file link handling
emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"),
emit_file_links: !vs_code_term,
editor,
};

Ok((slf, state_file_status))
}

#[inline]
pub fn current_exercise_ind(&self) -> usize {
self.current_exercise_ind
}

#[inline]
pub fn exercises(&self) -> &[Exercise] {
&self.exercises
}

#[inline]
pub fn n_done(&self) -> u32 {
self.n_done
}

#[inline]
pub fn n_pending(&self) -> u32 {
self.exercises.len() as u32 - self.n_done
}

#[inline]
pub fn current_exercise(&self) -> &Exercise {
&self.exercises[self.current_exercise_ind]
}

#[inline]
pub fn cmd_runner(&self) -> &CmdRunner {
&self.cmd_runner
}

#[inline]
pub fn emit_file_links(&self) -> bool {
self.emit_file_links
}
Expand Down Expand Up @@ -336,12 +335,10 @@ impl AppState {
Ok(())
}

pub fn reset_current_exercise(&mut self) -> Result<&str> {
pub fn reset_current_exercise(&mut self) -> Result<()> {
self.set_pending(self.current_exercise_ind)?;
let exercise = self.current_exercise();
self.reset(self.current_exercise_ind, &exercise.path)?;

Ok(&exercise.path)
self.reset(self.current_exercise_ind, exercise.path)
}

// Reset the exercise by index and return its name.
Expand All @@ -352,7 +349,7 @@ impl AppState {

self.set_pending(exercise_ind)?;
let exercise = &self.exercises[exercise_ind];
self.reset(exercise_ind, &exercise.path)?;
self.reset(exercise_ind, exercise.path)?;

Ok(exercise.name)
}
Expand Down Expand Up @@ -560,6 +557,28 @@ impl AppState {

Ok(())
}

pub fn open_editor(&mut self) -> Result<EditorJoinHandle> {
if let Some(editor) = self.editor.take() {
return editor.open(self.current_exercise_ind, self.current_exercise().path);
}

Ok(EditorJoinHandle::default())
}

pub fn join_editor_handle(&mut self, handle: EditorJoinHandle) -> Result<()> {
self.editor = handle.join()?;

Ok(())
}

pub fn close_editor(&mut self) -> Result<()> {
if let Some(editor) = &mut self.editor {
editor.close()?;
}

Ok(())
}
}

const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
Expand Down Expand Up @@ -594,7 +613,7 @@ mod tests {
Exercise {
name: "0",
dir: None,
path: String::from("exercises/0.rs"),
path: "exercises/0.rs",
canonical_path: None,
test: false,
strict_clippy: false,
Expand All @@ -615,6 +634,7 @@ mod tests {
official_exercises: true,
cmd_runner: CmdRunner::build().unwrap(),
emit_file_links: true,
editor: None,
};

let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
Expand Down
57 changes: 57 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use clap::{Parser, Subcommand};

use crate::dev::DevCommand;

/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
/// Disable automatic opening of the current file in VS Code or Zellij.
/// Ignores `--edit-cmd`
#[arg(long)]
pub no_editor: bool,
/// Open the current exercise by running `EDIT_CMD EXERCISE_PATH`.
/// The command is not allowed to block (e.g. `vim`).
/// It should communicate with an editor in a different process.
/// `EDIT_CMD` can contain arguments like `--edit-cmd "PROGRAM -x --arg1"`.
/// The current exercise's path is added by Rustlings as the last argument.
/// `--edit-cmd` is ignored in VS Code.
///
/// Example: `--edit-cmd "code"` (default behavior if running in a VS Code terminal)
#[arg(long)]
pub edit_cmd: Option<String>,
/// Manually run the current exercise using `r` in the watch mode.
/// Only use this if Rustlings fails to detect exercise file changes
#[arg(long)]
pub manual_run: bool,
}

#[derive(Subcommand)]
pub enum Command {
/// Initialize the official Rustlings exercises
Init,
/// Run a single exercise.
/// Runs the next pending exercise if the exercise name is not specified
Run {
/// The name of the exercise
name: Option<String>,
},
/// Check all the exercises, marking them as done or pending accordingly
CheckAll,
/// Reset a single exercise
Reset {
/// The name of the exercise
name: String,
},
/// Show a hint.
/// Shows the hint of the next pending exercise if the exercise name is not specified
Hint {
/// The name of the exercise
name: Option<String>,
},
/// Commands for developing (community) Rustlings exercises
#[command(subcommand)]
Dev(DevCommand),
}
Loading