Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: open current exercise via --editor #2204

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>

Expand Down
21 changes: 21 additions & 0 deletions src/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crossterm::{
QueueableCommand,
};
use std::io::{self, StdoutLock, Write};
use std::process::Command;

use crate::{
cmd::CmdRunner,
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 9 additions & 3 deletions src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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))]
{
Expand All @@ -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)?;
Expand All @@ -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"
Expand Down
19 changes: 19 additions & 0 deletions src/watch/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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<ExercisesProgress> {
// Ignore any input until checking all exercises is done.
let _input_pause_guard = InputPauseGuard::scoped_pause();
Expand Down
2 changes: 2 additions & 0 deletions src/watch/terminal_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum InputEvent {
CheckAll,
Reset,
Quit,
Edit,
}

pub fn terminal_event_handler(
Expand Down Expand Up @@ -51,6 +52,7 @@ pub fn terminal_event_handler(

continue;
}
KeyCode::Char('e') => InputEvent::Edit,
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
_ => continue,
};
Expand Down
Loading