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
1 change: 1 addition & 0 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 crates/mq-repl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mq-markdown = {workspace = true}
regex-lite = {workspace = true}
rustyline = {workspace = true, default-features = false, features = ["custom-bindings", "with-file-history"]}
strum = {workspace = true, features = ["derive"]}
tempfile = {workspace = true}

[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = {workspace = true, optional = true}
Expand Down
60 changes: 59 additions & 1 deletion crates/mq-repl/src/command_context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fmt, fs};
use std::{fmt, fs, io::Write, process::Command as ProcessCommand};

#[cfg(all(feature = "clipboard", not(target_os = "android")))]
use arboard::Clipboard;
Expand All @@ -15,6 +15,7 @@ pub enum CommandOutput {
#[derive(Debug, Clone, strum::EnumIter)]
pub enum Command {
Copy,
Edit,
Env(String, String),
Help,
Quit,
Expand All @@ -29,6 +30,7 @@ impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Command::Copy => write!(f, "/copy"),
Command::Edit => write!(f, "/edit"),
Command::Env(_, _) => {
write!(f, "/env")
}
Expand All @@ -47,6 +49,7 @@ impl Command {
pub fn help(&self) -> String {
match self {
Command::Copy => format!("{:<12}{}", "/copy", "Copy the execution results to the clipboard"),
Command::Edit => format!("{:<12}{}", "/edit", "Edit the current buffer in external editor"),
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text says 'Edit the current buffer' but the implementation starts with an empty file. Update the help text to accurately describe the behavior, such as 'Open external editor to write mq code' or similar.

Copilot generated this review using guidance from repository custom instructions.
Command::Env(_, _) => {
format!("{:<12}{}", "/env", "Set environment variables (key value)")
}
Expand All @@ -65,6 +68,7 @@ impl From<String> for Command {
fn from(s: String) -> Self {
match s.as_str().split_whitespace().collect::<Vec<&str>>().as_slice() {
["/copy"] => Command::Copy,
["/edit"] => Command::Edit,
["/env", name, value] => Command::Env(name.to_string(), value.to_string()),
["/help"] => Command::Help,
["/quit"] => Command::Quit,
Expand Down Expand Up @@ -135,6 +139,57 @@ impl CommandContext {
Err(miette!("Clipboard functionality is not available on this platform"))
}
}
Command::Edit => {
// Get editor from environment variables
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vi".to_string());

// Create a temporary file
let mut temp_file = tempfile::Builder::new()
.prefix("mq-edit-")
.suffix(".mq")
.tempfile()
.into_diagnostic()?;

// Write current buffer to temp file (empty for now)
temp_file.write_all(b"").into_diagnostic()?;
temp_file.flush().into_diagnostic()?;
Comment on lines +155 to +157
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment 'empty for now' suggests incomplete implementation. If the intention is to allow users to edit existing buffer content, this should populate the temporary file with current input or buffer state. Otherwise, clarify why starting with an empty file is the desired behavior.

Suggested change
// Write current buffer to temp file (empty for now)
temp_file.write_all(b"").into_diagnostic()?;
temp_file.flush().into_diagnostic()?;
// Start with an empty temp file; the user will provide code in the editor.

Copilot uses AI. Check for mistakes.

let temp_path = temp_file.path().to_path_buf();

// Close the file before opening in editor
drop(temp_file);

// Launch the editor
let status = ProcessCommand::new(&editor)
.arg(&temp_path)
.status()
.into_diagnostic()?;

if !status.success() {
return Err(miette!("Editor exited with non-zero status"));
}

// Read the edited content
let edited_content = fs::read_to_string(&temp_path).into_diagnostic()?;

// Clean up temp file
fs::remove_file(&temp_path).ok();
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently ignoring cleanup errors with .ok() could hide issues. Consider logging a warning if cleanup fails, or use a comment explaining why failures are acceptable here.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +177 to +178
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .ok() silently ignores any cleanup errors. Consider logging a warning if cleanup fails, as this could lead to accumulation of temporary files over time, especially if the REPL is used in long-running sessions.

Suggested change
// Clean up temp file
fs::remove_file(&temp_path).ok();
// Clean up temp file (best-effort; log on failure)
if let Err(err) = fs::remove_file(&temp_path) {
eprintln!(
"warning: failed to remove temporary file {}: {}",
temp_path.display(),
err
);
}

Copilot uses AI. Check for mistakes.

// Evaluate the edited content
let code = edited_content.trim();
if code.is_empty() {
Ok(CommandOutput::None)
} else {
let eval_result = self.engine.eval(code, self.input.clone().into_iter()).map_err(|e| *e)?;
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When evaluation fails, the error is propagated without additional context about the source (external editor). Consider wrapping the error with context like 'Failed to evaluate code from external editor' to help users understand where the error originated.

Copilot uses AI. Check for mistakes.

self.hir.add_line_of_code(self.source_id, self.scope_id, code);
self.input = eval_result.values().clone();

Ok(CommandOutput::Value(eval_result.values().clone()))
}
}
Command::Env(name, value) => {
unsafe { std::env::set_var(name, value) };
Ok(CommandOutput::None)
Expand Down Expand Up @@ -255,6 +310,7 @@ mod tests {
#[test]
fn test_command_from_string() {
assert!(matches!(Command::from("/copy".to_string()), Command::Copy));
assert!(matches!(Command::from("/edit".to_string()), Command::Edit));
assert!(matches!(Command::from("/help".to_string()), Command::Help));
assert!(matches!(Command::from("/quit".to_string()), Command::Quit));
assert!(matches!(Command::from("/vars".to_string()), Command::Vars));
Expand Down Expand Up @@ -283,6 +339,7 @@ mod tests {
#[test]
fn test_command_display() {
assert_eq!(format!("{}", Command::Copy), "/copy");
assert_eq!(format!("{}", Command::Edit), "/edit");
assert_eq!(format!("{}", Command::Help), "/help");
assert_eq!(format!("{}", Command::Quit), "/quit");
assert_eq!(format!("{}", Command::Vars), "/vars");
Expand All @@ -303,6 +360,7 @@ mod tests {

match cmd {
Command::Copy => assert!(help.contains("/copy")),
Command::Edit => assert!(help.contains("/edit")),
Command::Help => assert!(help.contains("/help")),
Command::Quit => assert!(help.contains("/quit")),
Command::Vars => assert!(help.contains("/vars")),
Expand Down
7 changes: 6 additions & 1 deletion crates/mq-repl/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::command_context::{Command, CommandContext, CommandOutput};
fn highlight_mq_syntax(line: &str) -> Cow<'_, str> {
let mut result = line.to_string();

let commands_pattern = r"^(/copy|/env|/help|/quit|/load|/vars|/version)\b";
let commands_pattern = r"^(/copy|/edit|/env|/help|/quit|/load|/vars|/version)\b";
if let Ok(re) = regex_lite::Regex::new(commands_pattern) {
result = re
.replace_all(&result, |caps: &regex_lite::Captures| {
Expand Down Expand Up @@ -232,6 +232,11 @@ impl Repl {
KeyEvent(KeyCode::Char('c'), Modifiers::ALT),
Cmd::Kill(Movement::WholeBuffer),
);
// Bind Esc+O (Alt+O) to open editor
editor.bind_sequence(
KeyEvent(KeyCode::Char('o'), Modifiers::ALT),
Cmd::Insert(1, "/edit\n".to_string()),
);

let config_dir = config_dir();

Expand Down