This commit is contained in:
pacexy 2025-03-15 09:20:29 +08:00 committed by GitHub
commit 8f341591d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 69 additions and 4 deletions

View file

@ -108,6 +108,16 @@ After the [initialization](#initialization), Rustlings can be launched by simply
This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers).
It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
You can specify an editor command with the `--editor` option to open exercises directly from watch mode:
```bash
rustlings --editor code # For VS Code
rustlings --editor vim # For Vim
rustlings --editor "code --wait" # For VS Code with wait argument
```
Then press `e` in watch mode to open the current exercise in your editor.
<details>
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>

View file

@ -4,6 +4,7 @@ use crossterm::{
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
};
use std::io::{self, StdoutLock, Write};
use std::process::Command;
use crate::{
cmd::CmdRunner,
@ -79,6 +80,26 @@ impl Exercise {
writer.write_str(self.path)
}
/// Open the exercise file in the specified editor
pub fn open_in_editor(&self, editor: &str) -> io::Result<bool> {
let parts: Vec<&str> = editor.split_whitespace().collect();
if parts.is_empty() {
return Ok(false);
}
let mut cmd = Command::new(parts[0]);
// If the editor command has arguments, add them to the command
if parts.len() > 1 {
cmd.args(&parts[1..]);
}
cmd.arg(self.path);
let status = cmd.status()?;
Ok(status.success())
}
}
pub trait RunnableExercise {

View file

@ -35,6 +35,9 @@ struct Args {
/// Only use this if Rustlings fails to detect exercise file changes.
#[arg(long)]
manual_run: bool,
/// Command to open exercise files in an editor (e.g. "code" for VS Code)
#[arg(long)]
editor: Option<String>,
}
#[derive(Subcommand)]
@ -135,7 +138,11 @@ fn main() -> Result<ExitCode> {
)
};
watch::watch(&mut app_state, notify_exercise_names)?;
watch::watch(
&mut app_state,
notify_exercise_names,
args.editor.as_deref(),
)?;
}
Some(Subcommands::Run { name }) => {
if let Some(name) = name {

View file

@ -62,6 +62,7 @@ enum WatchExit {
fn run_watch(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
editor: Option<&str>,
) -> Result<WatchExit> {
let (watch_event_sender, watch_event_receiver) = channel();
@ -113,6 +114,9 @@ fn run_watch(
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
},
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Edit) => {
watch_state.edit_exercise(&mut stdout, editor)?
}
WatchEvent::Input(InputEvent::Quit) => {
stdout.write_all(QUIT_MSG)?;
break;
@ -136,9 +140,10 @@ fn run_watch(
fn watch_list_loop(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
editor: Option<&str>,
) -> Result<()> {
loop {
match run_watch(app_state, notify_exercise_names)? {
match run_watch(app_state, notify_exercise_names, editor)? {
WatchExit::Shutdown => break Ok(()),
// It is much easier to exit the watch mode, launch the list mode and then restart
// the watch mode instead of trying to pause the watch threads and correct the
@ -152,6 +157,7 @@ fn watch_list_loop(
pub fn watch(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
editor: Option<&str>,
) -> Result<()> {
#[cfg(not(windows))]
{
@ -163,7 +169,7 @@ pub fn watch(
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
let res = watch_list_loop(app_state, notify_exercise_names);
let res = watch_list_loop(app_state, notify_exercise_names, editor);
termios.local_modes = original_local_modes;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
@ -172,7 +178,7 @@ pub fn watch(
}
#[cfg(windows)]
watch_list_loop(app_state, notify_exercise_names)
watch_list_loop(app_state, notify_exercise_names, editor)
}
const QUIT_MSG: &[u8] = b"

View file

@ -200,6 +200,7 @@ impl<'a> WatchState<'a> {
show_key(b'l', b":list / ")?;
show_key(b'c', b":check all / ")?;
show_key(b'x', b":reset / ")?;
show_key(b'e', b":edit / ")?;
show_key(b'q', b":quit ? ")?;
stdout.flush()
@ -269,6 +270,24 @@ impl<'a> WatchState<'a> {
Ok(())
}
pub fn edit_exercise(
&mut self,
stdout: &mut StdoutLock,
editor: Option<&str>,
) -> io::Result<()> {
if let Some(editor) = editor {
if let Err(e) = self.app_state.current_exercise().open_in_editor(editor) {
writeln!(stdout, "Failed to open editor: {}", e)?;
}
} else {
writeln!(
stdout,
"No editor command specified. Use --editor to specify an editor."
)?;
}
Ok(())
}
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
// Ignore any input until checking all exercises is done.
let _input_pause_guard = InputPauseGuard::scoped_pause();

View file

@ -14,6 +14,7 @@ pub enum InputEvent {
CheckAll,
Reset,
Quit,
Edit,
}
pub fn terminal_event_handler(
@ -51,6 +52,7 @@ pub fn terminal_event_handler(
continue;
}
KeyCode::Char('e') => InputEvent::Edit,
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
_ => continue,
};