From 1dc16e67098018112d8cd838537904381d3aaa89 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 10 Mar 2023 09:52:40 +0800 Subject: [PATCH] feat: type `{` `[` `(` to enter multi-line editing mode (#58) Abandon `.editor` command, it's too complicated. --- src/config/role.rs | 2 +- src/repl/init.rs | 49 ++----------------- src/repl/mod.rs | 115 +++++++++++++++++++++++++++++---------------- src/repl/prompt.rs | 4 +- 4 files changed, 81 insertions(+), 89 deletions(-) diff --git a/src/config/role.rs b/src/config/role.rs index 5155ea1..16f7bc1 100644 --- a/src/config/role.rs +++ b/src/config/role.rs @@ -39,7 +39,7 @@ impl Role { if self.embeded() { merge_prompt_content(&self.prompt, content) } else { - format!("{}{content}", self.prompt) + format!("{}\n{content}", self.prompt) } } diff --git a/src/repl/init.rs b/src/repl/init.rs index 61351fc..7d0c2bb 100644 --- a/src/repl/init.rs +++ b/src/repl/init.rs @@ -4,8 +4,8 @@ use crate::config::{Config, SharedConfig}; use anyhow::{Context, Result}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, FileBackedHistory, KeyCode, - KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, ValidationResult, Validator, + default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultValidator, Emacs, + FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, }; const MENU_NAME: &str = "completion_menu"; @@ -16,11 +16,6 @@ pub struct Repl { impl Repl { pub fn init(config: SharedConfig) -> Result { - let multiline_commands: Vec<&'static str> = REPL_COMMANDS - .iter() - .filter(|(_, _, v)| *v) - .map(|(v, _, _)| *v) - .collect(); let completer = Self::create_completer(config); let keybindings = Self::create_keybindings(); let history = Self::create_history()?; @@ -33,7 +28,7 @@ impl Repl { .with_edit_mode(edit_mode) .with_quick_completions(true) .with_partial_completions(true) - .with_validator(Box::new(ReplValidator { multiline_commands })) + .with_validator(Box::new(DefaultValidator)) .with_ansi_colors(true); Ok(Self { editor }) } @@ -41,7 +36,7 @@ impl Repl { fn create_completer(config: SharedConfig) -> DefaultCompleter { let mut completion: Vec = REPL_COMMANDS .into_iter() - .map(|(v, _, _)| v.to_string()) + .map(|(v, _)| v.to_string()) .collect(); completion.extend(config.lock().repl_completions()); let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2); @@ -79,39 +74,3 @@ impl Repl { )) } } - -struct ReplValidator { - multiline_commands: Vec<&'static str>, -} - -impl Validator for ReplValidator { - fn validate(&self, line: &str) -> ValidationResult { - if line.split('"').count() % 2 == 0 || incomplete_brackets(line, &self.multiline_commands) { - ValidationResult::Incomplete - } else { - ValidationResult::Complete - } - } -} - -fn incomplete_brackets(line: &str, multiline_commands: &[&str]) -> bool { - let mut balance: Vec = Vec::new(); - let line = line.trim_start(); - if !multiline_commands.iter().any(|v| line.starts_with(v)) { - return false; - } - - for c in line.chars() { - if c == '{' { - balance.push('}'); - } else if c == '}' { - if let Some(last) = balance.last() { - if last == &c { - balance.pop(); - } - } - } - } - - !balance.is_empty() -} diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 9d7d1cf..b9a6328 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -15,21 +15,21 @@ use crate::term; use anyhow::{Context, Result}; use reedline::Signal; +use std::borrow::Cow; use std::sync::Arc; -pub const REPL_COMMANDS: [(&str, &str, bool); 12] = [ - (".info", "Print the information", false), - (".set", "Modify the configuration temporarily", false), - (".prompt", "Add a GPT prompt", true), - (".role", "Select a role", false), - (".clear role", "Clear the currently selected role", false), - (".conversation", "Start a conversation.", false), - (".clear conversation", "End current conversation.", false), - (".history", "Print the history", false), - (".clear history", "Clear the history", false), - (".editor", "Enter editor mode for multiline input", true), - (".help", "Print this help message", false), - (".exit", "Exit the REPL", false), +pub const REPL_COMMANDS: [(&str, &str); 11] = [ + (".info", "Print the information"), + (".set", "Modify the configuration temporarily"), + (".prompt", "Add a GPT prompt"), + (".role", "Select a role"), + (".clear role", "Clear the currently selected role"), + (".conversation", "Start a conversation."), + (".clear conversation", "End current conversation."), + (".history", "Print the history"), + (".clear history", "Clear the history"), + (".help", "Print this help message"), + (".exit", "Exit the REPL"), ]; impl Repl { @@ -85,14 +85,9 @@ impl Repl { } fn handle_line(&mut self, handler: Arc, line: String) -> Result { - let mut trimed_line = line.trim_start(); - if trimed_line.starts_with('.') { - trimed_line = trimed_line.trim_end(); - let (cmd, args) = match trimed_line.split_once(' ') { - Some((head, tail)) => (head, Some(tail)), - None => (trimed_line, None), - }; - match cmd { + let line = clean_multiline_symbols(&line); + match parse_command(&line) { + Some((cmd, args)) => match cmd { ".exit" => { return Ok(true); } @@ -121,28 +116,14 @@ impl Repl { ".info" => { handler.handle(ReplCmd::ViewInfo)?; } - ".editor" => { - let mut text = args.unwrap_or_default().to_string(); - if text.is_empty() { - print_now!("Usage: .editor {{ }}\n\n"); - } else { - if text.starts_with('{') && text.ends_with('}') { - text = text[1..text.len() - 1].to_string() - } - handler.handle(ReplCmd::Submit(text))?; - } - } ".set" => { handler.handle(ReplCmd::UpdateConfig(args.unwrap_or_default().to_string()))? } ".prompt" => { - let mut text = args.unwrap_or_default().to_string(); + let text = args.unwrap_or_default().to_string(); if text.is_empty() { - print_now!("Usage: .prompt {{ }}.\n\n"); + print_now!("Usage: .prompt .\n\n"); } else { - if text.starts_with('{') && text.ends_with('}') { - text = text[1..text.len() - 1].to_string() - } handler.handle(ReplCmd::Prompt(text))?; } } @@ -150,9 +131,10 @@ impl Repl { handler.handle(ReplCmd::StartConversation)?; } _ => dump_unknown_command(), + }, + None => { + handler.handle(ReplCmd::Submit(line.to_string()))?; } - } else { - handler.handle(ReplCmd::Submit(line))?; } Ok(false) @@ -166,7 +148,7 @@ fn dump_unknown_command() { fn dump_repl_help() { let head = REPL_COMMANDS .iter() - .map(|(name, desc, _)| format!("{name:<24} {desc}")) + .map(|(name, desc)| format!("{name:<24} {desc}")) .collect::>() .join("\n"); print_now!( @@ -174,3 +156,56 @@ fn dump_repl_help() { head, ); } + +fn clean_multiline_symbols(line: &str) -> Cow { + let trimed_line = line.trim(); + match trimed_line.chars().next() { + Some('{') | Some('[') | Some('(') => trimed_line[1..trimed_line.len() - 1].into(), + _ => Cow::Borrowed(line), + } +} + +fn parse_command(line: &str) -> Option<(&str, Option<&str>)> { + let mut trimed_line = line.trim_start(); + if trimed_line.starts_with('.') { + trimed_line = trimed_line.trim_end(); + match trimed_line + .split_once(' ') + .or_else(|| trimed_line.split_once('\n')) + { + Some((head, tail)) => { + let trimed_tail = tail.trim(); + if trimed_tail.is_empty() { + Some((head, None)) + } else { + Some((head, Some(trimed_tail))) + } + } + None => Some((trimed_line, None)), + } + } else { + None + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_process_command_line() { + assert_eq!(parse_command(" .role"), Some((".role", None))); + assert_eq!(parse_command(" .role "), Some((".role", None))); + assert_eq!( + parse_command(" .set dry_run true"), + Some((".set", Some("dry_run true"))) + ); + assert_eq!( + parse_command(" .set dry_run true "), + Some((".set", Some("dry_run true"))) + ); + assert_eq!( + parse_command(".prompt \nabc\n"), + Some((".prompt", Some("abc"))) + ); + } +} diff --git a/src/repl/prompt.rs b/src/repl/prompt.rs index 4c56626..dd32ff8 100644 --- a/src/repl/prompt.rs +++ b/src/repl/prompt.rs @@ -3,8 +3,6 @@ use crate::config::SharedConfig; use reedline::{Prompt, PromptHistorySearch, PromptHistorySearchStatus}; use std::borrow::Cow; -const DEFAULT_MULTILINE_INDICATOR: &str = "::: "; - #[derive(Clone)] pub struct ReplPrompt(SharedConfig); @@ -43,7 +41,7 @@ impl Prompt for ReplPrompt { } fn render_prompt_multiline_indicator(&self) -> Cow { - Cow::Borrowed(DEFAULT_MULTILINE_INDICATOR) + Cow::Borrowed("") } fn render_prompt_history_search_indicator(