diff --git a/README.md b/README.md index 3118451f80..90e37f404d 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,16 @@ After [initialization](#initialization), Rustlings can be launched by simply run 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. +
If detecting file changes in the exercises/ directory fails… (click to expand) diff --git a/src/exercise.rs b/src/exercise.rs index 849082847a..6d2cd7839e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -4,6 +4,7 @@ use crossterm::{ QueueableCommand, }; 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 { + 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 { diff --git a/src/main.rs b/src/main.rs index eeb1883edd..cd849bb659 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, } #[derive(Subcommand)] @@ -135,7 +138,11 @@ fn main() -> Result { ) }; - 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 { diff --git a/src/watch.rs b/src/watch.rs index 3a56b4b65b..ad4a10f736 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -62,6 +62,7 @@ enum WatchExit { fn run_watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + editor: Option<&str>, ) -> Result { 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" diff --git a/src/watch/state.rs b/src/watch/state.rs index 5263bc5788..1ad07bce04 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -199,6 +199,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() @@ -268,6 +269,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 { // Ignore any input until checking all exercises is done. let _input_pause_guard = InputPauseGuard::scoped_pause(); diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 48411db0f7..3c32323dc2 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -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, };