cl-frontend: Improve the line-editing capabilities in REPL mode.
- Note: This adds a dependency on Crossterm, to set the terminal into raw mode. This could be eliminated by linking the libraries Crossterm uses directly... Or I could embrace Crossterm, and use it for all escape sequences in the terminal, instead of assuming DEC VT500 compatibility.
This commit is contained in:
parent
5e4e70a72e
commit
77db95791a
@ -11,3 +11,4 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
conlang = { path = "../libconlang" }
|
||||
crossterm = "0.27.0"
|
||||
|
@ -199,7 +199,6 @@ pub mod cli {
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
error::Error,
|
||||
io::{stdin, stdout, Write},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
@ -358,32 +357,12 @@ pub mod cli {
|
||||
|
||||
/// Prompt functions
|
||||
impl Repl {
|
||||
pub fn prompt_begin(&self) {
|
||||
print!(
|
||||
"{}{} {ANSI_RESET}",
|
||||
self.mode.ansi_color(),
|
||||
self.prompt_begin
|
||||
);
|
||||
let _ = stdout().flush();
|
||||
}
|
||||
pub fn prompt_again(&self) {
|
||||
print!(
|
||||
"{}{} {ANSI_RESET}",
|
||||
self.mode.ansi_color(),
|
||||
self.prompt_again
|
||||
);
|
||||
let _ = stdout().flush();
|
||||
}
|
||||
pub fn prompt_error(&self, err: &impl Error) {
|
||||
println!("{ANSI_RED}{} {err}{ANSI_RESET}", self.prompt_error)
|
||||
}
|
||||
pub fn begin_output(&self) {
|
||||
print!("{ANSI_OUTPUT}")
|
||||
}
|
||||
fn reprompt(&self, buf: &mut String) {
|
||||
self.prompt_begin();
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
/// The actual REPL
|
||||
impl Repl {
|
||||
@ -393,56 +372,62 @@ pub mod cli {
|
||||
}
|
||||
/// Runs the main REPL loop
|
||||
pub fn repl(&mut self) {
|
||||
let mut buf = String::new();
|
||||
self.prompt_begin();
|
||||
while let Ok(len) = stdin().read_line(&mut buf) {
|
||||
use crate::repline::Repline;
|
||||
let mut rl = Repline::new(
|
||||
// std::fs::File::open("/dev/stdin").unwrap(),
|
||||
self.mode.ansi_color(),
|
||||
self.prompt_begin,
|
||||
self.prompt_again,
|
||||
);
|
||||
// self.prompt_begin();
|
||||
while let Ok(line) = rl.read() {
|
||||
// Exit the loop
|
||||
if len == 0 {
|
||||
if line.is_empty() {
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
self.begin_output();
|
||||
// Process mode-change commands
|
||||
if self.command(&buf) {
|
||||
self.reprompt(&mut buf);
|
||||
if self.command(&line) {
|
||||
rl.accept();
|
||||
rl.set_color(self.mode.ansi_color());
|
||||
continue;
|
||||
}
|
||||
// Lex the buffer, or reset and output the error
|
||||
let code = Program::new(&buf);
|
||||
let code = Program::new(&line);
|
||||
match code.lex().into_iter().find(|l| l.is_err()) {
|
||||
None => (),
|
||||
Some(Ok(_)) => unreachable!(),
|
||||
Some(Err(error)) => {
|
||||
eprintln!("{error}");
|
||||
self.reprompt(&mut buf);
|
||||
rl.deny();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// TODO: only lex the program once
|
||||
// Tokenize mode doesn't require valid parse, so it gets processed first
|
||||
if self.mode == Mode::Tokenize {
|
||||
self.tokenize(&code);
|
||||
self.reprompt(&mut buf);
|
||||
rl.deny();
|
||||
continue;
|
||||
}
|
||||
// Parse code and dispatch to the proper function
|
||||
match (len, code.parse()) {
|
||||
match (line.len(), code.parse()) {
|
||||
(0, Ok(_)) => {
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
// If the code is OK, run it and print any errors
|
||||
(_, Ok(mut code)) => {
|
||||
println!();
|
||||
self.dispatch(&mut code);
|
||||
buf.clear();
|
||||
rl.accept();
|
||||
}
|
||||
// If the user types two newlines, print syntax errors
|
||||
(1, Err(e)) => {
|
||||
self.prompt_error(&e);
|
||||
buf.clear();
|
||||
}
|
||||
// Otherwise, ask for more input
|
||||
_ => {
|
||||
self.prompt_again();
|
||||
(_, Err(e)) => {
|
||||
print!("\x1b[100G\x1b[37m//{e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.prompt_begin()
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,3 +491,5 @@ pub mod cli {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod repline;
|
||||
|
@ -2,8 +2,5 @@ use cl_frontend::{args::Args, cli::CLI};
|
||||
use std::error::Error;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// parse args
|
||||
let args = Args::new().parse().unwrap_or_default();
|
||||
let mut cli = CLI::from(args);
|
||||
cli.run()
|
||||
CLI::from(Args::new().parse().unwrap_or_default()).run()
|
||||
}
|
||||
|
481
cl-frontend/src/repline.rs
Normal file
481
cl-frontend/src/repline.rs
Normal file
@ -0,0 +1,481 @@
|
||||
//! A small pseudo-multiline editing library
|
||||
// #![allow(unused)]
|
||||
|
||||
pub mod error {
|
||||
/// Result type for Repline
|
||||
pub type ReplResult<T> = std::result::Result<T, Error>;
|
||||
/// Borrowed error (does not implement [Error](std::error::Error)!)
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// User broke with Ctrl+C
|
||||
CtrlC(String),
|
||||
/// User broke with Ctrl+D
|
||||
CtrlD(String),
|
||||
/// Invalid unicode codepoint
|
||||
BadUnicode(u32),
|
||||
/// Error came from [std::io]
|
||||
IoFailure(std::io::Error),
|
||||
/// End of input
|
||||
EndOfInput,
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::CtrlC(_) => write!(f, "Ctrl+C"),
|
||||
Error::CtrlD(_) => write!(f, "Ctrl+D"),
|
||||
Error::BadUnicode(u) => write!(f, "0x{u:x} is not a valid unicode codepoint"),
|
||||
Error::IoFailure(s) => write!(f, "{s}"),
|
||||
Error::EndOfInput => write!(f, "End of input"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoFailure(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod ignore {
|
||||
//! Does nothing, universally.
|
||||
//!
|
||||
//! Introduces the [Ignore] trait, and its singular function, [ignore](Ignore::ignore),
|
||||
//! which does nothing.
|
||||
impl<T> Ignore for T {}
|
||||
/// Does nothing
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// #![deny(unused_must_use)]
|
||||
/// # use cl_frontend::repline::ignore::Ignore;
|
||||
/// ().ignore();
|
||||
/// Err::<(), &str>("Foo").ignore();
|
||||
/// Some("Bar").ignore();
|
||||
/// 42.ignore();
|
||||
///
|
||||
/// #[must_use]
|
||||
/// fn the_meaning() -> usize {
|
||||
/// 42
|
||||
/// }
|
||||
/// the_meaning().ignore();
|
||||
/// ```
|
||||
pub trait Ignore {
|
||||
/// Does nothing
|
||||
fn ignore(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod chars {
|
||||
//! Converts an <code>[Iterator]<Item = [u8]></code> into an
|
||||
//! <code>[Iterator]<Item = [char]></code>
|
||||
|
||||
use super::error::*;
|
||||
|
||||
/// Converts an <code>[Iterator]<Item = [u8]></code> into an
|
||||
/// <code>[Iterator]<Item = [char]></code>
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Chars<I: Iterator<Item = u8>>(pub I);
|
||||
impl<I: Iterator<Item = u8>> Chars<I> {
|
||||
pub fn new(bytes: I) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
|
||||
type Item = ReplResult<char>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let Self(bytes) = self;
|
||||
let start = bytes.next()? as u32;
|
||||
let (mut out, count) = match start {
|
||||
start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range
|
||||
start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte
|
||||
start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes
|
||||
start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes
|
||||
_ => return None,
|
||||
};
|
||||
for _ in 0..count {
|
||||
let cont = bytes.next()? as u32;
|
||||
if cont & 0xc0 != 0x80 {
|
||||
return None;
|
||||
}
|
||||
out = out << 6 | (cont & 0x3f);
|
||||
}
|
||||
Some(char::from_u32(out).ok_or(Error::BadUnicode(out)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod flatten {
|
||||
//! Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
||||
//! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
||||
|
||||
/// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
||||
/// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
||||
pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
|
||||
impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
|
||||
type Item = T;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()?.ok()
|
||||
}
|
||||
}
|
||||
impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
|
||||
type Item = T;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod raw {
|
||||
//! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime.
|
||||
|
||||
/// Sets the terminal to raw mode for the duration of the returned object's lifetime.
|
||||
pub fn raw() -> impl Drop {
|
||||
Raw::default()
|
||||
}
|
||||
struct Raw();
|
||||
impl Default for Raw {
|
||||
fn default() -> Self {
|
||||
std::thread::yield_now();
|
||||
crossterm::terminal::enable_raw_mode()
|
||||
.expect("should be able to transition into raw mode");
|
||||
Raw()
|
||||
}
|
||||
}
|
||||
impl Drop for Raw {
|
||||
fn drop(&mut self) {
|
||||
crossterm::terminal::disable_raw_mode()
|
||||
.expect("should be able to transition out of raw mode");
|
||||
// std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod out {
|
||||
#![allow(unused)]
|
||||
use std::io::{Result, Write};
|
||||
|
||||
/// A [Writer](Write) that flushes after every wipe
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct EagerWriter<W: Write> {
|
||||
out: W,
|
||||
}
|
||||
impl<W: Write> EagerWriter<W> {
|
||||
pub fn new(writer: W) -> Self {
|
||||
Self { out: writer }
|
||||
}
|
||||
}
|
||||
impl<W: Write> Write for EagerWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
let out = self.out.write(buf)?;
|
||||
self.out.flush()?;
|
||||
Ok(out)
|
||||
}
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.out.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use self::{chars::Chars, editor::Editor, error::*, flatten::Flatten, ignore::Ignore, raw::raw};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{stdout, Bytes, Read, Result, Write},
|
||||
};
|
||||
|
||||
pub struct Repline<'a, R: Read> {
|
||||
input: Chars<Flatten<Result<u8>, Bytes<R>>>,
|
||||
|
||||
history: VecDeque<String>, // previous lines
|
||||
hindex: usize, // current index into the history buffer
|
||||
|
||||
ed: Editor<'a>, // the current line buffer
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Repline<'a, R> {
|
||||
/// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts.
|
||||
pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||
Self {
|
||||
input: Chars(Flatten(input.bytes())),
|
||||
history: Default::default(),
|
||||
hindex: 0,
|
||||
ed: Editor::new(color, begin, again),
|
||||
}
|
||||
}
|
||||
/// Set the terminal prompt color
|
||||
pub fn set_color(&mut self, color: &'a str) {
|
||||
self.ed.color = color
|
||||
}
|
||||
/// Reads in a line, and returns it for validation
|
||||
pub fn read(&mut self) -> ReplResult<String> {
|
||||
const INDENT: &str = " ";
|
||||
let mut stdout = stdout().lock();
|
||||
let _make_raw = raw();
|
||||
self.ed.begin_frame(&mut stdout)?;
|
||||
self.ed.render(&mut stdout)?;
|
||||
loop {
|
||||
stdout.flush()?;
|
||||
self.ed.begin_frame(&mut stdout)?;
|
||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||
// Ctrl+C: End of Text. Immediately exits.
|
||||
// Ctrl+D: End of Transmission. Ends the current line.
|
||||
'\x03' => {
|
||||
drop(_make_raw);
|
||||
return Err(Error::CtrlC(self.ed.to_string()));
|
||||
}
|
||||
'\x04' => {
|
||||
drop(_make_raw);
|
||||
writeln!(stdout).ignore();
|
||||
self.ed.render(&mut stdout)?; // TODO: this, better
|
||||
return Ok(self.ed.to_string());
|
||||
// return Err(Error::CtrlD(self.ed.to_string()));
|
||||
}
|
||||
// Tab: extend line by 4 spaces
|
||||
'\t' => {
|
||||
self.ed.extend(INDENT.chars());
|
||||
}
|
||||
// ignore newlines, process line feeds. Not sure how cross-platform this is.
|
||||
'\n' => {}
|
||||
'\r' => {
|
||||
self.ed.push('\n').ignore();
|
||||
self.ed.render(&mut stdout)?;
|
||||
return Ok(self.ed.to_string());
|
||||
}
|
||||
// Escape sequence
|
||||
'\x1b' => self.escape()?,
|
||||
// backspace
|
||||
'\x08' | '\x7f' => {
|
||||
let ed = &mut self.ed;
|
||||
if ed.ends_with(INDENT.chars()) {
|
||||
for _ in 0..INDENT.len() {
|
||||
ed.pop().ignore();
|
||||
}
|
||||
} else {
|
||||
self.ed.pop().ignore();
|
||||
}
|
||||
}
|
||||
c if c.is_ascii_control() => {
|
||||
eprint!("\\x{:02x}", c as u32);
|
||||
}
|
||||
c => {
|
||||
self.ed.push(c).unwrap();
|
||||
}
|
||||
}
|
||||
self.ed.render(&mut stdout)?;
|
||||
}
|
||||
}
|
||||
/// Handle ANSI Escape
|
||||
fn escape(&mut self) -> ReplResult<()> {
|
||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||
'[' => self.csi()?,
|
||||
'O' => todo!("Process alternate character mode"),
|
||||
other => self.ed.extend(['\x1b', other]),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Handle ANSI Control Sequence Introducer
|
||||
fn csi(&mut self) -> ReplResult<()> {
|
||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||
'A' => {
|
||||
self.hindex = self.hindex.saturating_sub(1);
|
||||
self.restore_history();
|
||||
}
|
||||
'B' => {
|
||||
self.hindex = self
|
||||
.hindex
|
||||
.saturating_add(1)
|
||||
.min(self.history.len().saturating_sub(1));
|
||||
self.restore_history();
|
||||
}
|
||||
'C' => self.ed.cursor_back(1).ignore(),
|
||||
'D' => self.ed.cursor_forward(1).ignore(),
|
||||
'3' => {
|
||||
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
|
||||
self.ed.delete().ignore()
|
||||
}
|
||||
}
|
||||
other => {
|
||||
eprintln!("{other}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Restores the currently selected history
|
||||
pub fn restore_history(&mut self) {
|
||||
let Self { history, hindex, ed, .. } = self;
|
||||
if !(0..history.len()).contains(hindex) {
|
||||
return;
|
||||
};
|
||||
ed.clear();
|
||||
ed.extend(
|
||||
history
|
||||
.get(*hindex)
|
||||
.expect("history should contain index")
|
||||
.trim_end_matches(|c: char| c != '\n' && c.is_whitespace())
|
||||
.chars(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Append line to history and clear it
|
||||
pub fn accept(&mut self) {
|
||||
self.history_append(self.ed.iter().collect());
|
||||
self.ed.clear();
|
||||
self.hindex = self.history.len();
|
||||
}
|
||||
/// Append line to history
|
||||
pub fn history_append(&mut self, buf: String) {
|
||||
if !self.history.contains(&buf) {
|
||||
self.history.push_back(buf)
|
||||
}
|
||||
while self.history.len() > 20 {
|
||||
self.history.pop_front();
|
||||
}
|
||||
}
|
||||
/// Clear the line
|
||||
pub fn deny(&mut self) {}
|
||||
}
|
||||
|
||||
impl<'a> Repline<'a, std::io::Stdin> {
|
||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||
Self::with_input(std::io::stdin(), color, begin, again)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod editor {
|
||||
use std::{collections::VecDeque, fmt::Display, io::Write};
|
||||
|
||||
use super::error::{Error, ReplResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor<'a> {
|
||||
head: VecDeque<char>,
|
||||
tail: VecDeque<char>,
|
||||
|
||||
pub color: &'a str,
|
||||
begin: &'a str,
|
||||
again: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Editor<'a> {
|
||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||
Self { head: Default::default(), tail: Default::default(), color, begin, again }
|
||||
}
|
||||
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
||||
self.head.iter()
|
||||
}
|
||||
pub fn begin_frame<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||
let Self { head, .. } = self;
|
||||
match head.iter().filter(|&&c| c == '\n').count() {
|
||||
0 => write!(w, "\x1b[0G"),
|
||||
lines => write!(w, "\x1b[{}F", lines),
|
||||
}?;
|
||||
write!(w, "\x1b[0J")?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||
let Self { head, tail, color, begin, again } = self;
|
||||
write!(w, "{color}{begin}\x1b[0m ")?;
|
||||
// draw head
|
||||
for c in head {
|
||||
match c {
|
||||
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
||||
_ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()),
|
||||
}?
|
||||
}
|
||||
// save cursor
|
||||
write!(w, "\x1b[s")?;
|
||||
// draw tail
|
||||
for c in tail {
|
||||
match c {
|
||||
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
||||
_ => write!(w, "{c}"),
|
||||
}?
|
||||
}
|
||||
// restore cursor
|
||||
w.write_all(b"\x1b[u").map_err(Into::into)
|
||||
}
|
||||
pub fn restore(&mut self, s: &str) {
|
||||
self.clear();
|
||||
self.head.extend(s.chars())
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.head.clear();
|
||||
self.tail.clear();
|
||||
}
|
||||
pub fn push(&mut self, c: char) -> ReplResult<()> {
|
||||
self.head.push_back(c);
|
||||
Ok(())
|
||||
}
|
||||
pub fn pop(&mut self) -> ReplResult<char> {
|
||||
self.head.pop_back().ok_or(Error::EndOfInput)
|
||||
}
|
||||
pub fn delete(&mut self) -> ReplResult<char> {
|
||||
self.tail.pop_front().ok_or(Error::EndOfInput)
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.head.len() + self.tail.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.head.is_empty() && self.tail.is_empty()
|
||||
}
|
||||
pub fn ends_with(&self, iter: impl DoubleEndedIterator<Item = char>) -> bool {
|
||||
let mut iter = iter.rev();
|
||||
let mut head = self.head.iter().rev();
|
||||
loop {
|
||||
match (iter.next(), head.next()) {
|
||||
(None, _) => break true,
|
||||
(Some(_), None) => break false,
|
||||
(Some(a), Some(b)) if a != *b => break false,
|
||||
(Some(_), Some(_)) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Moves the cursor back `steps` steps
|
||||
pub fn cursor_forward(&mut self, steps: usize) -> Option<()> {
|
||||
let Self { head, tail, .. } = self;
|
||||
for _ in 0..steps {
|
||||
tail.push_front(head.pop_back()?);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
/// Moves the cursor forward `steps` steps
|
||||
pub fn cursor_back(&mut self, steps: usize) -> Option<()> {
|
||||
let Self { head, tail, .. } = self;
|
||||
for _ in 0..steps {
|
||||
head.push_back(tail.pop_front()?);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<char> for Editor<'a> {
|
||||
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
|
||||
self.head.extend(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'e> IntoIterator for &'e Editor<'a> {
|
||||
type Item = &'e char;
|
||||
type IntoIter = std::iter::Chain<
|
||||
std::collections::vec_deque::Iter<'e, char>,
|
||||
std::collections::vec_deque::Iter<'e, char>,
|
||||
>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.head.iter().chain(self.tail.iter())
|
||||
}
|
||||
}
|
||||
impl<'a> Display for Editor<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use std::fmt::Write;
|
||||
let Self { head, tail, .. } = self;
|
||||
for c in head {
|
||||
f.write_char(*c)?;
|
||||
}
|
||||
for c in tail {
|
||||
f.write_char(*c)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user