cl-frontend: Embrace the crossterm jank
This commit is contained in:
parent
e01c71f9a7
commit
78b8d87408
@ -379,13 +379,7 @@ pub mod cli {
|
|||||||
/// Runs the main REPL loop
|
/// Runs the main REPL loop
|
||||||
pub fn repl(&mut self) {
|
pub fn repl(&mut self) {
|
||||||
use crate::repline::{error::Error, Repline};
|
use crate::repline::{error::Error, Repline};
|
||||||
let mut rl = Repline::new(
|
let mut rl = Repline::new(self.mode.ansi_color(), self.prompt_begin, self.prompt_again);
|
||||||
// std::fs::File::open("/dev/stdin").unwrap(),
|
|
||||||
self.mode.ansi_color(),
|
|
||||||
self.prompt_begin,
|
|
||||||
self.prompt_again,
|
|
||||||
);
|
|
||||||
// self.prompt_begin();
|
|
||||||
fn clear_line() {
|
fn clear_line() {
|
||||||
print!("\x1b[G\x1b[J");
|
print!("\x1b[G\x1b[J");
|
||||||
}
|
}
|
||||||
@ -397,16 +391,17 @@ pub mod cli {
|
|||||||
if buf.is_empty() || buf.ends_with('\n') {
|
if buf.is_empty() || buf.ends_with('\n') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rl.accept();
|
||||||
|
println!("Cancelled. (Press Ctrl+C again to quit.)");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Ctrl-D: reset input, and parse it for errors
|
// Ctrl-D: reset input, and parse it for errors
|
||||||
Err(Error::CtrlD(buf)) => {
|
Err(Error::CtrlD(buf)) => {
|
||||||
|
rl.deny();
|
||||||
if let Err(e) = Program::new(&buf).parse() {
|
if let Err(e) = Program::new(&buf).parse() {
|
||||||
println!();
|
|
||||||
clear_line();
|
clear_line();
|
||||||
self.prompt_error(&e);
|
self.prompt_error(&e);
|
||||||
}
|
}
|
||||||
rl.deny();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -432,7 +427,7 @@ pub mod cli {
|
|||||||
Some(Ok(_)) => unreachable!(),
|
Some(Ok(_)) => unreachable!(),
|
||||||
Some(Err(error)) => {
|
Some(Err(error)) => {
|
||||||
rl.deny();
|
rl.deny();
|
||||||
eprintln!("{error}");
|
self.prompt_error(&error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,109 +211,112 @@ impl<'a, R: Read> Repline<'a, R> {
|
|||||||
pub fn read(&mut self) -> ReplResult<String> {
|
pub fn read(&mut self) -> ReplResult<String> {
|
||||||
const INDENT: &str = " ";
|
const INDENT: &str = " ";
|
||||||
let mut stdout = stdout().lock();
|
let mut stdout = stdout().lock();
|
||||||
|
let stdout = &mut stdout;
|
||||||
let _make_raw = raw();
|
let _make_raw = raw();
|
||||||
self.ed.begin_frame(&mut stdout)?;
|
// self.ed.begin_frame(stdout)?;
|
||||||
self.ed.render(&mut stdout)?;
|
// self.ed.redraw_frame(stdout)?;
|
||||||
|
self.ed.print_head(stdout)?;
|
||||||
loop {
|
loop {
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
self.ed.begin_frame(&mut stdout)?;
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
// Ctrl+C: End of Text. Immediately exits.
|
// Ctrl+C: End of Text. Immediately exits.
|
||||||
// Ctrl+D: End of Transmission. Ends the current line.
|
// Ctrl+D: End of Transmission. Ends the current line.
|
||||||
'\x03' => {
|
'\x03' => {
|
||||||
self.ed.render(&mut stdout)?;
|
|
||||||
drop(_make_raw);
|
drop(_make_raw);
|
||||||
|
writeln!(stdout)?;
|
||||||
return Err(Error::CtrlC(self.ed.to_string()));
|
return Err(Error::CtrlC(self.ed.to_string()));
|
||||||
}
|
}
|
||||||
'\x04' => {
|
'\x04' => {
|
||||||
self.ed.render(&mut stdout)?; // TODO: this, better
|
|
||||||
drop(_make_raw);
|
drop(_make_raw);
|
||||||
|
writeln!(stdout)?;
|
||||||
return Err(Error::CtrlD(self.ed.to_string()));
|
return Err(Error::CtrlD(self.ed.to_string()));
|
||||||
}
|
}
|
||||||
// Tab: extend line by 4 spaces
|
// Tab: extend line by 4 spaces
|
||||||
'\t' => {
|
'\t' => {
|
||||||
self.ed.extend(INDENT.chars());
|
self.ed.extend(INDENT.chars(), stdout)?;
|
||||||
}
|
}
|
||||||
// ignore newlines, process line feeds. Not sure how cross-platform this is.
|
// ignore newlines, process line feeds. Not sure how cross-platform this is.
|
||||||
'\n' => {}
|
'\n' => {}
|
||||||
'\r' => {
|
'\r' => {
|
||||||
self.ed.push('\n').ignore();
|
self.ed.push('\n', stdout)?;
|
||||||
self.ed.render(&mut stdout)?;
|
|
||||||
return Ok(self.ed.to_string());
|
return Ok(self.ed.to_string());
|
||||||
}
|
}
|
||||||
// Escape sequence
|
// Escape sequence
|
||||||
'\x1b' => self.escape()?,
|
'\x1b' => self.escape(stdout)?,
|
||||||
// backspace
|
// backspace
|
||||||
'\x08' | '\x7f' => {
|
'\x08' | '\x7f' => {
|
||||||
let ed = &mut self.ed;
|
let ed = &mut self.ed;
|
||||||
if ed.ends_with(INDENT.chars()) {
|
if ed.ends_with(INDENT.chars()) {
|
||||||
for _ in 0..INDENT.len() {
|
for _ in 0..INDENT.len() {
|
||||||
ed.pop().ignore();
|
ed.pop(stdout)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.ed.pop().ignore();
|
ed.pop(stdout)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c if c.is_ascii_control() => {
|
c if c.is_ascii_control() => {
|
||||||
eprint!("\\x{:02x}", c as u32);
|
eprint!("\\x{:02x}", c as u32);
|
||||||
}
|
}
|
||||||
c => {
|
c => {
|
||||||
self.ed.push(c).unwrap();
|
self.ed.push(c, stdout)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.ed.render(&mut stdout)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Handle ANSI Escape
|
/// Handle ANSI Escape
|
||||||
fn escape(&mut self) -> ReplResult<()> {
|
fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
'[' => self.csi()?,
|
'[' => self.csi(w)?,
|
||||||
'O' => todo!("Process alternate character mode"),
|
'O' => todo!("Process alternate character mode"),
|
||||||
other => self.ed.extend(['\x1b', other]),
|
other => self.ed.extend(['\x1b', other], w)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Handle ANSI Control Sequence Introducer
|
/// Handle ANSI Control Sequence Introducer
|
||||||
fn csi(&mut self) -> ReplResult<()> {
|
fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
'A' => {
|
'A' => {
|
||||||
self.hindex = self.hindex.saturating_sub(1);
|
self.hindex = self.hindex.saturating_sub(1);
|
||||||
self.restore_history();
|
self.restore_history(w)?
|
||||||
}
|
}
|
||||||
'B' => {
|
'B' => {
|
||||||
self.hindex = self
|
self.hindex = self
|
||||||
.hindex
|
.hindex
|
||||||
.saturating_add(1)
|
.saturating_add(1)
|
||||||
.min(self.history.len().saturating_sub(1));
|
.min(self.history.len().saturating_sub(1));
|
||||||
self.restore_history();
|
self.restore_history(w)?
|
||||||
}
|
}
|
||||||
'C' => self.ed.cursor_back(1).ignore(),
|
'C' => self.ed.cursor_forward(1, w)?,
|
||||||
'D' => self.ed.cursor_forward(1).ignore(),
|
'D' => self.ed.cursor_back(1, w)?,
|
||||||
|
'H' => self.ed.home(w)?,
|
||||||
|
'F' => self.ed.end(w)?,
|
||||||
'3' => {
|
'3' => {
|
||||||
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
|
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
self.ed.delete().ignore()
|
self.ed.delete(w).ignore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
eprintln!("{other}");
|
eprint!("{}", other.escape_unicode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Restores the currently selected history
|
/// Restores the currently selected history
|
||||||
pub fn restore_history(&mut self) {
|
pub fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
let Self { history, hindex, ed, .. } = self;
|
let Self { history, hindex, ed, .. } = self;
|
||||||
if !(0..history.len()).contains(hindex) {
|
if !(0..history.len()).contains(hindex) {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
ed.undraw(w)?;
|
||||||
ed.clear();
|
ed.clear();
|
||||||
|
ed.print_head(w)?;
|
||||||
ed.extend(
|
ed.extend(
|
||||||
history
|
history
|
||||||
.get(*hindex)
|
.get(*hindex)
|
||||||
.expect("history should contain index")
|
.expect("history should contain index")
|
||||||
.trim_end_matches(|c: char| c != '\n' && c.is_whitespace())
|
|
||||||
.chars(),
|
.chars(),
|
||||||
);
|
w,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append line to history and clear it
|
/// Append line to history and clear it
|
||||||
@ -323,7 +326,10 @@ impl<'a, R: Read> Repline<'a, R> {
|
|||||||
self.hindex = self.history.len();
|
self.hindex = self.history.len();
|
||||||
}
|
}
|
||||||
/// Append line to history
|
/// Append line to history
|
||||||
pub fn history_append(&mut self, buf: String) {
|
pub fn history_append(&mut self, mut buf: String) {
|
||||||
|
while buf.ends_with(char::is_whitespace) {
|
||||||
|
buf.pop();
|
||||||
|
}
|
||||||
if !self.history.contains(&buf) {
|
if !self.history.contains(&buf) {
|
||||||
self.history.push_back(buf)
|
self.history.push_back(buf)
|
||||||
}
|
}
|
||||||
@ -344,10 +350,25 @@ impl<'a> Repline<'a, std::io::Stdin> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod editor {
|
pub mod editor {
|
||||||
|
use crossterm::{cursor::*, execute, queue, style::*, terminal::*};
|
||||||
use std::{collections::VecDeque, fmt::Display, io::Write};
|
use std::{collections::VecDeque, fmt::Display, io::Write};
|
||||||
|
|
||||||
use super::error::{Error, ReplResult};
|
use super::error::{Error, ReplResult};
|
||||||
|
|
||||||
|
fn is_newline(c: &char) -> bool {
|
||||||
|
*c == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_chars<'a, W: Write>(
|
||||||
|
c: impl IntoIterator<Item = &'a char>,
|
||||||
|
w: &mut W,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
for c in c {
|
||||||
|
write!(w, "{c}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Editor<'a> {
|
pub struct Editor<'a> {
|
||||||
head: VecDeque<char>,
|
head: VecDeque<char>,
|
||||||
@ -365,16 +386,17 @@ pub mod editor {
|
|||||||
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
||||||
self.head.iter()
|
self.head.iter()
|
||||||
}
|
}
|
||||||
pub fn begin_frame<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
pub fn undraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
let Self { head, .. } = self;
|
let Self { head, .. } = self;
|
||||||
match head.iter().filter(|&&c| c == '\n').count() {
|
match head.iter().copied().filter(is_newline).count() {
|
||||||
0 => write!(w, "\x1b[0G"),
|
0 => write!(w, "\x1b[0G"),
|
||||||
lines => write!(w, "\x1b[{}F", lines),
|
lines => write!(w, "\x1b[{}F", lines),
|
||||||
}?;
|
}?;
|
||||||
write!(w, "\x1b[0J")?;
|
queue!(w, Clear(ClearType::FromCursorDown))?;
|
||||||
|
// write!(w, "\x1b[0J")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn render<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
pub fn redraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
let Self { head, tail, color, begin, again } = self;
|
let Self { head, tail, color, begin, again } = self;
|
||||||
write!(w, "{color}{begin}\x1b[0m ")?;
|
write!(w, "{color}{begin}\x1b[0m ")?;
|
||||||
// draw head
|
// draw head
|
||||||
@ -385,7 +407,7 @@ pub mod editor {
|
|||||||
}?
|
}?
|
||||||
}
|
}
|
||||||
// save cursor
|
// save cursor
|
||||||
write!(w, "\x1b[s")?;
|
execute!(w, SavePosition)?;
|
||||||
// draw tail
|
// draw tail
|
||||||
for c in tail {
|
for c in tail {
|
||||||
match c {
|
match c {
|
||||||
@ -394,7 +416,99 @@ pub mod editor {
|
|||||||
}?
|
}?
|
||||||
}
|
}
|
||||||
// restore cursor
|
// restore cursor
|
||||||
w.write_all(b"\x1b[u").map_err(Into::into)
|
execute!(w, RestorePosition)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn prompt<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
|
let Self { head, color, begin, again, .. } = self;
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
MoveToColumn(0),
|
||||||
|
Print(color),
|
||||||
|
Print(if head.is_empty() { begin } else { again }),
|
||||||
|
ResetColor,
|
||||||
|
Print(' '),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn print_head<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
|
self.prompt(w)?;
|
||||||
|
write_chars(
|
||||||
|
self.head.iter().skip(
|
||||||
|
self.head
|
||||||
|
.iter()
|
||||||
|
.rposition(is_newline)
|
||||||
|
.unwrap_or(self.head.len())
|
||||||
|
+ 1,
|
||||||
|
),
|
||||||
|
w,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn print_tail<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
|
let Self { tail, .. } = self;
|
||||||
|
queue!(w, SavePosition, Clear(ClearType::UntilNewLine))?;
|
||||||
|
write_chars(tail.iter().take_while(|&c| !is_newline(c)), w)?;
|
||||||
|
queue!(w, RestorePosition)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn push<W: Write>(&mut self, c: char, w: &mut W) -> ReplResult<()> {
|
||||||
|
// Tail optimization: if the tail is empty,
|
||||||
|
//we don't have to undraw and redraw on newline
|
||||||
|
if self.tail.is_empty() {
|
||||||
|
self.head.push_back(c);
|
||||||
|
match c {
|
||||||
|
'\n' => {
|
||||||
|
write!(w, "\r\n")?;
|
||||||
|
self.print_head(w)?;
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
queue!(w, Print(c))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if '\n' == c {
|
||||||
|
self.undraw(w)?;
|
||||||
|
}
|
||||||
|
self.head.push_back(c);
|
||||||
|
match c {
|
||||||
|
'\n' => self.redraw(w)?,
|
||||||
|
_ => {
|
||||||
|
write!(w, "{c}")?;
|
||||||
|
self.print_tail(w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn pop<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
|
||||||
|
if let Some('\n') = self.head.back() {
|
||||||
|
self.undraw(w)?;
|
||||||
|
}
|
||||||
|
let c = self.head.pop_back();
|
||||||
|
// if the character was a newline, we need to go back a line
|
||||||
|
match c {
|
||||||
|
Some('\n') => self.redraw(w)?,
|
||||||
|
Some(_) => {
|
||||||
|
// go back a char
|
||||||
|
queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?;
|
||||||
|
self.print_tail(w)?;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend<T: IntoIterator<Item = char>, W: Write>(
|
||||||
|
&mut self,
|
||||||
|
iter: T,
|
||||||
|
w: &mut W,
|
||||||
|
) -> ReplResult<()> {
|
||||||
|
for c in iter {
|
||||||
|
self.push(c, w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn restore(&mut self, s: &str) {
|
pub fn restore(&mut self, s: &str) {
|
||||||
self.clear();
|
self.clear();
|
||||||
@ -404,15 +518,21 @@ pub mod editor {
|
|||||||
self.head.clear();
|
self.head.clear();
|
||||||
self.tail.clear();
|
self.tail.clear();
|
||||||
}
|
}
|
||||||
pub fn push(&mut self, c: char) -> ReplResult<()> {
|
pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
|
||||||
self.head.push_back(c);
|
match self.tail.front() {
|
||||||
Ok(())
|
Some('\n') => {
|
||||||
|
self.undraw(w)?;
|
||||||
|
let out = self.tail.pop_front();
|
||||||
|
self.redraw(w)?;
|
||||||
|
out
|
||||||
}
|
}
|
||||||
pub fn pop(&mut self) -> ReplResult<char> {
|
_ => {
|
||||||
self.head.pop_back().ok_or(Error::EndOfInput)
|
let out = self.tail.pop_front();
|
||||||
|
self.print_tail(w)?;
|
||||||
|
out
|
||||||
}
|
}
|
||||||
pub fn delete(&mut self) -> ReplResult<char> {
|
}
|
||||||
self.tail.pop_front().ok_or(Error::EndOfInput)
|
.ok_or(Error::EndOfInput)
|
||||||
}
|
}
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.head.len() + self.tail.len()
|
self.head.len() + self.tail.len()
|
||||||
@ -433,26 +553,56 @@ pub mod editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Moves the cursor back `steps` steps
|
/// Moves the cursor back `steps` steps
|
||||||
pub fn cursor_forward(&mut self, steps: usize) -> Option<()> {
|
pub fn cursor_back<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
||||||
let Self { head, tail, .. } = self;
|
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
tail.push_front(head.pop_back()?);
|
if let Some('\n') = self.head.back() {
|
||||||
|
self.undraw(w)?;
|
||||||
}
|
}
|
||||||
Some(())
|
let Some(c) = self.head.pop_back() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
self.tail.push_front(c);
|
||||||
|
match c {
|
||||||
|
'\n' => self.redraw(w)?,
|
||||||
|
_ => queue!(w, MoveLeft(1))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
/// Moves the cursor forward `steps` steps
|
/// Moves the cursor forward `steps` steps
|
||||||
pub fn cursor_back(&mut self, steps: usize) -> Option<()> {
|
pub fn cursor_forward<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
||||||
let Self { head, tail, .. } = self;
|
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
head.push_back(tail.pop_front()?);
|
if let Some('\n') = self.tail.front() {
|
||||||
|
self.undraw(w)?
|
||||||
}
|
}
|
||||||
Some(())
|
let Some(c) = self.tail.pop_front() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
self.head.push_back(c);
|
||||||
|
match c {
|
||||||
|
'\n' => self.redraw(w)?,
|
||||||
|
_ => queue!(w, MoveRight(1))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Goes to the beginning of the current line
|
||||||
|
pub fn home<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
loop {
|
||||||
|
match self.head.back() {
|
||||||
|
Some('\n') | None => break Ok(()),
|
||||||
|
Some(_) => self.cursor_back(1, w)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Goes to the end of the current line
|
||||||
|
pub fn end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
loop {
|
||||||
|
match self.tail.front() {
|
||||||
|
Some('\n') | None => break Ok(()),
|
||||||
|
Some(_) => self.cursor_forward(1, w)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Extend<char> for Editor<'a> {
|
|
||||||
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
|
|
||||||
self.head.extend(iter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user