From aadffb17a3cbbfc5e3c0dad4c8587eff79aaae9b Mon Sep 17 00:00:00 2001 From: sigoden Date: Sun, 5 Mar 2023 19:16:41 +0800 Subject: feat: repl command replaces `.editor` with `.multiline` (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can use `.multiline` command to put multiline content ``` 〉.multiline { ::: abc ::: cdf ::: } ``` It's much easier than `.editor`. --- src/repl.rs | 65 ++++++++++++++++++++++++++++--------- src/term.rs | 104 +---------------------------------------------------------- src/utils.rs | 6 ---- 3 files changed, 50 insertions(+), 125 deletions(-) (limited to 'src') diff --git a/src/repl.rs b/src/repl.rs index 2a5947f..b507d89 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -9,7 +9,7 @@ use crossbeam::sync::WaitGroup; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, DefaultPromptSegment, Emacs, FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, - ReedlineMenu, Signal, + ReedlineMenu, Signal, ValidationResult, Validator, }; use std::cell::RefCell; use std::fs::File; @@ -21,10 +21,10 @@ use std::thread::spawn; const REPL_COMMANDS: [(&str, &str); 10] = [ (".role", "Specifies the role the AI will play"), (".clear role", "Clear the currently selected role"), - (".editor", "Enter editor mode"), - (".copy", "Copy last reply message"), (".history", "Print the history"), (".clear history", "Clear the history"), + (".multiline", "Enter multiline editor mode"), + (".copy", "Copy last reply message"), (".info", "Print the information"), (".help", "Print this help message"), (".exit", "Exit the REPL"), @@ -51,7 +51,9 @@ impl Repl { .with_menu(menu) .with_edit_mode(edit_mode) .with_quick_completions(true) - .with_partial_completions(true); + .with_partial_completions(true) + .with_validator(Box::new(ReplValidator)) + .with_ansi_colors(true); let prompt = Self::create_prompt(); Ok(Self { editor, prompt }) } @@ -132,14 +134,16 @@ impl Repl { Some(name) => handler.handle(ReplCmd::SetRole(name.to_string()))?, None => dump("Usage: .role ", 2), }, - ".editor" => { - dump( - "// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)", - 1, - ); - let content = term::edit()?; - dump("", 1); - handler.handle(ReplCmd::Submit(content))?; + ".info" => { + handler.handle(ReplCmd::Info)?; + } + ".multiline" => { + let text = args.unwrap_or_default().to_string(); + if text.starts_with('{') && text.ends_with('}') { + handler.handle(ReplCmd::Submit(text))?; + } else { + dump("Usage: .multiline { put your content here }", 2); + } } ".copy" => { let reply = handler.get_reply(); @@ -147,12 +151,9 @@ impl Repl { dump("No reply messages that can be copied", 1) } else { copy(&reply)?; - dump("Pasted", 1); + dump("Copied", 1); } } - ".info" => { - handler.handle(ReplCmd::Info)?; - } _ => dump_unknown_command(), } } else { @@ -207,6 +208,38 @@ impl Repl { )) } } +pub struct ReplValidator; + +impl Validator for ReplValidator { + fn validate(&self, line: &str) -> ValidationResult { + if line.split('"').count() % 2 == 0 || incomplete_brackets(line) { + ValidationResult::Incomplete + } else { + ValidationResult::Complete + } + } +} + +fn incomplete_brackets(line: &str) -> bool { + let mut balance: Vec = Vec::new(); + if !line.trim_start().starts_with(".multiline") { + 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() +} pub struct ReplCmdHandler { client: ChatGptClient, diff --git a/src/term.rs b/src/term.rs index 0d2780e..05eedde 100644 --- a/src/term.rs +++ b/src/term.rs @@ -3,112 +3,10 @@ use std::io::{self, Stdout, Write}; use anyhow::Result; use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyModifiers}, - queue, style, + cursor, queue, style, terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; -use crate::utils::paste; - -pub fn edit() -> Result { - enable_raw_mode()?; - let mut stdout = io::stdout(); - - let ret = edit_inner(&mut stdout); - - // restore terminal - disable_raw_mode()?; - - ret -} - -fn edit_inner(writer: &mut Stdout) -> Result { - let mut session = Session::new(writer); - - loop { - let evt = event::read()?; - if let Event::Key(key) = evt { - match key.code { - KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { - // quit - return Ok(String::new()); - } - KeyCode::Char('d') if key.modifiers == KeyModifiers::CONTROL => { - // submit - return Ok(session.buffer); - } - KeyCode::Char('v') if key.modifiers == KeyModifiers::CONTROL => { - let content = paste()?; - session.push_str(&content)?; - } - KeyCode::Char(c) - if matches!(key.modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) => - { - session.push(c)?; - } - KeyCode::Enter => { - session.push('\n')?; - } - _ => {} - } - } - session.flush()?; - } -} - -struct Session<'a, T: Write> { - writer: &'a mut T, - buffer: String, - dirty: bool, -} - -impl<'a, T: Write> Session<'a, T> { - fn new<'b: 'a>(writer: &'b mut T) -> Self { - Self { - buffer: String::new(), - writer, - dirty: false, - } - } - fn push(&mut self, ch: char) -> io::Result<()> { - if ch == '\n' { - self.new_line()?; - } else { - queue!(self.writer, style::Print(ch))?; - } - self.buffer.push(ch); - self.dirty = true; - Ok(()) - } - fn push_str(&mut self, text: &str) -> io::Result<()> { - for line in text.lines() { - if !line.is_empty() { - queue!(self.writer, style::Print(line))?; - } - self.new_line()?; - } - - Ok(()) - } - fn new_line(&mut self) -> io::Result<()> { - let (_, y) = cursor::position()?; - let (_, h) = terminal::size()?; - if y == h - 1 { - queue!(self.writer, terminal::ScrollUp(1), cursor::MoveTo(0, y))?; - } else { - queue!(self.writer, cursor::MoveToNextLine(1))?; - } - Ok(()) - } - fn flush(&mut self) -> io::Result<()> { - if self.dirty { - return self.writer.flush(); - } - Ok(()) - } -} - pub fn clear_screen(keep_lines: u16) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); diff --git a/src/utils.rs b/src/utils.rs index 5bc1237..9dfbbd7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,12 +14,6 @@ pub fn copy(src: &str) -> Result<()> { .map_err(|err| anyhow!("Failed to copy, {err}")) } -pub fn paste() -> Result { - ClipboardContext::new() - .and_then(|mut ctx| ctx.get_contents()) - .map_err(|err| anyhow!("Failed to paste, {err}")) -} - pub fn now() -> String { let now = Local::now(); now.to_rfc3339_opts(SecondsFormat::Secs, false) -- cgit v1.2.3