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() {
merge_prompt_content(&self.prompt, content)
} 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 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<Self> {
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<String> = 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<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 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<ReplCmdHandler>, line: String) -> Result<bool> {
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 {{ <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" => {
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 {{ <your content here> }}.\n\n");
print_now!("Usage: .prompt <text>.\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::<Vec<String>>()
.join("\n");
print_now!(
@ -174,3 +156,56 @@ fn dump_repl_help() {
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 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<str> {
Cow::Borrowed(DEFAULT_MULTILINE_INDICATOR)
Cow::Borrowed("")
}
fn render_prompt_history_search_indicator(