feat: type { [ ( to enter multi-line editing mode (#58)

Abandon `.editor` command, it's too complicated.
This commit is contained in:
sigoden 2023-03-10 09:52:40 +08:00 committed by GitHub
parent 54e6edbf21
commit 1dc16e6709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 89 deletions

View File

@ -39,7 +39,7 @@ impl Role {
if self.embeded() { if self.embeded() {
merge_prompt_content(&self.prompt, content) merge_prompt_content(&self.prompt, content)
} else { } else {
format!("{}{content}", self.prompt) format!("{}\n{content}", self.prompt)
} }
} }

View File

@ -4,8 +4,8 @@ use crate::config::{Config, SharedConfig};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use reedline::{ use reedline::{
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, FileBackedHistory, KeyCode, default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultValidator, Emacs,
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, ValidationResult, Validator, FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu,
}; };
const MENU_NAME: &str = "completion_menu"; const MENU_NAME: &str = "completion_menu";
@ -16,11 +16,6 @@ pub struct Repl {
impl Repl { impl Repl {
pub fn init(config: SharedConfig) -> Result<Self> { pub fn init(config: SharedConfig) -> Result<Self> {
let multiline_commands: Vec<&'static str> = REPL_COMMANDS
.iter()
.filter(|(_, _, v)| *v)
.map(|(v, _, _)| *v)
.collect();
let completer = Self::create_completer(config); let completer = Self::create_completer(config);
let keybindings = Self::create_keybindings(); let keybindings = Self::create_keybindings();
let history = Self::create_history()?; let history = Self::create_history()?;
@ -33,7 +28,7 @@ impl Repl {
.with_edit_mode(edit_mode) .with_edit_mode(edit_mode)
.with_quick_completions(true) .with_quick_completions(true)
.with_partial_completions(true) .with_partial_completions(true)
.with_validator(Box::new(ReplValidator { multiline_commands })) .with_validator(Box::new(DefaultValidator))
.with_ansi_colors(true); .with_ansi_colors(true);
Ok(Self { editor }) Ok(Self { editor })
} }
@ -41,7 +36,7 @@ impl Repl {
fn create_completer(config: SharedConfig) -> DefaultCompleter { fn create_completer(config: SharedConfig) -> DefaultCompleter {
let mut completion: Vec<String> = REPL_COMMANDS let mut completion: Vec<String> = REPL_COMMANDS
.into_iter() .into_iter()
.map(|(v, _, _)| v.to_string()) .map(|(v, _)| v.to_string())
.collect(); .collect();
completion.extend(config.lock().repl_completions()); completion.extend(config.lock().repl_completions());
let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2); 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<char> = 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()
}

View File

@ -15,21 +15,21 @@ use crate::term;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use reedline::Signal; use reedline::Signal;
use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
pub const REPL_COMMANDS: [(&str, &str, bool); 12] = [ pub const REPL_COMMANDS: [(&str, &str); 11] = [
(".info", "Print the information", false), (".info", "Print the information"),
(".set", "Modify the configuration temporarily", false), (".set", "Modify the configuration temporarily"),
(".prompt", "Add a GPT prompt", true), (".prompt", "Add a GPT prompt"),
(".role", "Select a role", false), (".role", "Select a role"),
(".clear role", "Clear the currently selected role", false), (".clear role", "Clear the currently selected role"),
(".conversation", "Start a conversation.", false), (".conversation", "Start a conversation."),
(".clear conversation", "End current conversation.", false), (".clear conversation", "End current conversation."),
(".history", "Print the history", false), (".history", "Print the history"),
(".clear history", "Clear the history", false), (".clear history", "Clear the history"),
(".editor", "Enter editor mode for multiline input", true), (".help", "Print this help message"),
(".help", "Print this help message", false), (".exit", "Exit the REPL"),
(".exit", "Exit the REPL", false),
]; ];
impl Repl { impl Repl {
@ -85,14 +85,9 @@ impl Repl {
} }
fn handle_line(&mut self, handler: Arc<ReplCmdHandler>, line: String) -> Result<bool> { fn handle_line(&mut self, handler: Arc<ReplCmdHandler>, line: String) -> Result<bool> {
let mut trimed_line = line.trim_start(); let line = clean_multiline_symbols(&line);
if trimed_line.starts_with('.') { match parse_command(&line) {
trimed_line = trimed_line.trim_end(); Some((cmd, args)) => match cmd {
let (cmd, args) = match trimed_line.split_once(' ') {
Some((head, tail)) => (head, Some(tail)),
None => (trimed_line, None),
};
match cmd {
".exit" => { ".exit" => {
return Ok(true); return Ok(true);
} }
@ -121,28 +116,14 @@ impl Repl {
".info" => { ".info" => {
handler.handle(ReplCmd::ViewInfo)?; handler.handle(ReplCmd::ViewInfo)?;
} }
".editor" => {
let mut text = args.unwrap_or_default().to_string();
if text.is_empty() {
print_now!("Usage: .editor {{ <your multiline/paste content here> }}\n\n");
} else {
if text.starts_with('{') && text.ends_with('}') {
text = text[1..text.len() - 1].to_string()
}
handler.handle(ReplCmd::Submit(text))?;
}
}
".set" => { ".set" => {
handler.handle(ReplCmd::UpdateConfig(args.unwrap_or_default().to_string()))? handler.handle(ReplCmd::UpdateConfig(args.unwrap_or_default().to_string()))?
} }
".prompt" => { ".prompt" => {
let mut text = args.unwrap_or_default().to_string(); let text = args.unwrap_or_default().to_string();
if text.is_empty() { if text.is_empty() {
print_now!("Usage: .prompt {{ <your content here> }}.\n\n"); print_now!("Usage: .prompt <text>.\n\n");
} else { } else {
if text.starts_with('{') && text.ends_with('}') {
text = text[1..text.len() - 1].to_string()
}
handler.handle(ReplCmd::Prompt(text))?; handler.handle(ReplCmd::Prompt(text))?;
} }
} }
@ -150,9 +131,10 @@ impl Repl {
handler.handle(ReplCmd::StartConversation)?; handler.handle(ReplCmd::StartConversation)?;
} }
_ => dump_unknown_command(), _ => dump_unknown_command(),
},
None => {
handler.handle(ReplCmd::Submit(line.to_string()))?;
} }
} else {
handler.handle(ReplCmd::Submit(line))?;
} }
Ok(false) Ok(false)
@ -166,7 +148,7 @@ fn dump_unknown_command() {
fn dump_repl_help() { fn dump_repl_help() {
let head = REPL_COMMANDS let head = REPL_COMMANDS
.iter() .iter()
.map(|(name, desc, _)| format!("{name:<24} {desc}")) .map(|(name, desc)| format!("{name:<24} {desc}"))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
print_now!( print_now!(
@ -174,3 +156,56 @@ fn dump_repl_help() {
head, head,
); );
} }
fn clean_multiline_symbols(line: &str) -> Cow<str> {
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")))
);
}
}

View File

@ -3,8 +3,6 @@ use crate::config::SharedConfig;
use reedline::{Prompt, PromptHistorySearch, PromptHistorySearchStatus}; use reedline::{Prompt, PromptHistorySearch, PromptHistorySearchStatus};
use std::borrow::Cow; use std::borrow::Cow;
const DEFAULT_MULTILINE_INDICATOR: &str = "::: ";
#[derive(Clone)] #[derive(Clone)]
pub struct ReplPrompt(SharedConfig); pub struct ReplPrompt(SharedConfig);
@ -43,7 +41,7 @@ impl Prompt for ReplPrompt {
} }
fn render_prompt_multiline_indicator(&self) -> Cow<str> { fn render_prompt_multiline_indicator(&self) -> Cow<str> {
Cow::Borrowed(DEFAULT_MULTILINE_INDICATOR) Cow::Borrowed("")
} }
fn render_prompt_history_search_indicator( fn render_prompt_history_search_indicator(