Skip to content

Commit 9e42ff8

Browse files
carlessoEnrico Carlesso
andauthored
feat(chat): Add /ed command to compose prompts in text editor (#1035)
* feat(chat): Add /ed command to compose prompts in text editor Implement the /ed command that opens the user's preferred editor to compose a prompt. The content is automatically submitted when the editor closes, improving the user experience for longer prompts. Fixes #1029 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * refactor(chat): Remove /ed command from masthead text Keep the /ed command in the help text but remove it from the masthead to avoid overloading users with too many commands when they are getting started. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * refactor(chat): Improve editor command naming and help text Rename command from /ed to /editor and improve the help text to clarify that vi is used as the default editor when nvim is not set. Keep the function name as open_editor for better code readability. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * refactor(chat): Change /ed command to /editor for clarity This change renames the /ed command to /editor in the command list to make its purpose more clear to users. The longer name is more descriptive and easier to understand for new users. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * refactor(q_cli): Convert open_editor to associated function Remove unused self parameter from open_editor method and update call site to use Self::open_editor() instead of self.open_editor(). 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * feat(chat): Enhance editor command to accept initial text Allow users to provide initial text with the /editor command that will be pre-populated in the editor. Also remove the default template text to provide a cleaner editing experience. This commit also fixes compilation errors by adding the missing pending_tool_index field to ChatState initializers in the editor command handling code. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) * style: Fix code formatting for editor command Update the formatting of the PromptEditor struct initialization to match Rust style guidelines. This fixes the cargo +nightly fmt check. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) --------- Co-authored-by: Enrico Carlesso <[email protected]>
1 parent 437f83d commit 9e42ff8

File tree

4 files changed

+138
-1
lines changed

4 files changed

+138
-1
lines changed

crates/q_cli/src/cli/chat/command.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum Command {
1717
Quit,
1818
Profile { subcommand: ProfileSubcommand },
1919
Context { subcommand: ContextSubcommand },
20+
PromptEditor { initial_text: Option<String> },
2021
Tools { subcommand: Option<ToolsSubcommand> },
2122
}
2223

@@ -232,6 +233,15 @@ impl Command {
232233
subcommand: Some(ToolsSubcommand::TrustAll),
233234
}
234235
},
236+
"editor" => {
237+
if parts.len() > 1 {
238+
Self::PromptEditor {
239+
initial_text: Some(parts[1..].join(" ")),
240+
}
241+
} else {
242+
Self::PromptEditor { initial_text: None }
243+
}
244+
},
235245
"issue" => {
236246
if parts.len() > 1 {
237247
Self::Issue {

crates/q_cli/src/cli/chat/input_source.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ impl InputSource {
5353
},
5454
}
5555
}
56+
57+
// We're keeping this method for potential future use
58+
#[allow(dead_code)]
59+
pub fn set_buffer(&mut self, content: &str) {
60+
if let inner::Inner::Readline(rl) = &mut self.0 {
61+
// Add to history so user can access it with up arrow
62+
let _ = rl.add_history_entry(content);
63+
}
64+
}
5665
}
5766

5867
#[cfg(test)]

crates/q_cli/src/cli/chat/mod.rs

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ use std::io::{
1616
Read,
1717
Write,
1818
};
19-
use std::process::ExitCode;
19+
use std::process::{
20+
Command as ProcessCommand,
21+
ExitCode,
22+
};
2023
use std::sync::Arc;
2124
use std::time::Duration;
25+
use std::{
26+
env,
27+
fs,
28+
};
2229

2330
use command::{
2431
Command,
@@ -85,6 +92,7 @@ use tracing::{
8592
trace,
8693
warn,
8794
};
95+
use uuid::Uuid;
8896
use winnow::Partial;
8997
use winnow::stream::Offset;
9098

@@ -122,6 +130,7 @@ const HELP_TEXT: &str = color_print::cstr! {"
122130
<cyan,em>Commands:</cyan,em>
123131
<em>/clear</em> <black!>Clear the conversation history</black!>
124132
<em>/issue</em> <black!>Report an issue or make a feature request</black!>
133+
<em>/editor</em> <black!>Open $EDITOR (defaults to vi) to compose a prompt</black!>
125134
<em>/help</em> <black!>Show this help dialogue</black!>
126135
<em>/quit</em> <black!>Quit the application</black!>
127136
<em>/tools</em> <black!>View and manage tools and permissions</black!>
@@ -418,6 +427,41 @@ impl<W> ChatContext<W>
418427
where
419428
W: Write,
420429
{
430+
/// Opens the user's preferred editor to compose a prompt
431+
fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
432+
// Create a temporary file with a unique name
433+
let temp_dir = std::env::temp_dir();
434+
let file_name = format!("q_prompt_{}.md", Uuid::new_v4());
435+
let temp_file_path = temp_dir.join(file_name);
436+
437+
// Get the editor from environment variable or use a default
438+
let editor = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
439+
440+
// Write initial content to the file if provided
441+
let initial_content = initial_text.unwrap_or_default();
442+
fs::write(&temp_file_path, &initial_content)
443+
.map_err(|e| ChatError::Custom(format!("Failed to create temporary file: {}", e).into()))?;
444+
445+
// Open the editor
446+
let status = ProcessCommand::new(editor)
447+
.arg(&temp_file_path)
448+
.status()
449+
.map_err(|e| ChatError::Custom(format!("Failed to open editor: {}", e).into()))?;
450+
451+
if !status.success() {
452+
return Err(ChatError::Custom("Editor exited with non-zero status".into()));
453+
}
454+
455+
// Read the content back
456+
let content = fs::read_to_string(&temp_file_path)
457+
.map_err(|e| ChatError::Custom(format!("Failed to read temporary file: {}", e).into()))?;
458+
459+
// Clean up the temporary file
460+
let _ = fs::remove_file(&temp_file_path);
461+
462+
Ok(content.trim().to_string())
463+
}
464+
421465
async fn try_chat(&mut self) -> Result<()> {
422466
if self.interactive && self.settings.get_bool_or("chat.greeting.enabled", true) {
423467
execute!(self.output, style::Print(WELCOME_TEXT))?;
@@ -780,6 +824,64 @@ where
780824
pending_tool_index,
781825
}
782826
},
827+
Command::PromptEditor { initial_text } => {
828+
match Self::open_editor(initial_text) {
829+
Ok(content) => {
830+
if content.trim().is_empty() {
831+
execute!(
832+
self.output,
833+
style::SetForegroundColor(Color::Yellow),
834+
style::Print("\nEmpty content from editor, not submitting.\n\n"),
835+
style::SetForegroundColor(Color::Reset)
836+
)?;
837+
838+
ChatState::PromptUser {
839+
tool_uses: Some(tool_uses),
840+
pending_tool_index,
841+
skip_printing_tools: true,
842+
}
843+
} else {
844+
execute!(
845+
self.output,
846+
style::SetForegroundColor(Color::Green),
847+
style::Print("\nContent loaded from editor. Submitting prompt...\n\n"),
848+
style::SetForegroundColor(Color::Reset)
849+
)?;
850+
851+
// Display the content as if the user typed it
852+
execute!(
853+
self.output,
854+
style::SetForegroundColor(Color::Magenta),
855+
style::Print("> "),
856+
style::SetAttribute(Attribute::Reset),
857+
style::Print(&content),
858+
style::Print("\n")
859+
)?;
860+
861+
// Process the content as user input
862+
ChatState::HandleInput {
863+
input: content,
864+
tool_uses: Some(tool_uses),
865+
pending_tool_index,
866+
}
867+
}
868+
},
869+
Err(e) => {
870+
execute!(
871+
self.output,
872+
style::SetForegroundColor(Color::Red),
873+
style::Print(format!("\nError opening editor: {}\n\n", e)),
874+
style::SetForegroundColor(Color::Reset)
875+
)?;
876+
877+
ChatState::PromptUser {
878+
tool_uses: Some(tool_uses),
879+
pending_tool_index,
880+
skip_printing_tools: true,
881+
}
882+
},
883+
}
884+
},
783885
Command::Quit => ChatState::Exit,
784886
Command::Profile { subcommand } => {
785887
if let Some(context_manager) = &mut self.conversation_state.context_manager {
@@ -1886,4 +1988,19 @@ mod tests {
18861988

18871989
assert_eq!(ctx.fs().read_to_string("/file.txt").await.unwrap(), "Hello, world!\n");
18881990
}
1991+
1992+
#[test]
1993+
fn test_editor_content_processing() {
1994+
// Since we no longer have template replacement, this test is simplified
1995+
let cases = vec![
1996+
("My content", "My content"),
1997+
("My content with newline\n", "My content with newline"),
1998+
("", ""),
1999+
];
2000+
2001+
for (input, expected) in cases {
2002+
let processed = input.trim().to_string();
2003+
assert_eq!(processed, expected.trim().to_string(), "Failed for input: {}", input);
2004+
}
2005+
}
18892006
}

crates/q_cli/src/cli/chat/prompt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use winnow::stream::AsChar;
3838
const COMMANDS: &[&str] = &[
3939
"/clear",
4040
"/help",
41+
"/editor",
4142
"/issue",
4243
// "/acceptall", /// Functional, but deprecated in favor of /tools trustall
4344
"/quit",

0 commit comments

Comments
 (0)